Comparing version 1.0.13 to 1.1.0
58
API.md
@@ -5,7 +5,11 @@ ## Classes | ||
<dd></dd> | ||
<dt><a href="#RedisCluster">RedisCluster</a> ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code></dt> | ||
<dd></dd> | ||
<dt><a href="#Commander">Commander</a></dt> | ||
<dd></dd> | ||
</dl> | ||
## Members | ||
<dl> | ||
<dt><a href="#defaultOptions">defaultOptions</a></dt> | ||
<dd><p>Default options</p> | ||
</dd> | ||
</dl> | ||
<a name="Redis"></a> | ||
@@ -77,3 +81,3 @@ ## Redis ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code> | ||
This method closes the connection immediately, | ||
and may lose some pending replies that haven't written to clien. | ||
and may lose some pending replies that haven't written to client. | ||
If you want to wait for the pending replies, use Redis#quit instead. | ||
@@ -153,44 +157,2 @@ | ||
**Kind**: static method of <code>[Redis](#Redis)</code> | ||
<a name="RedisCluster"></a> | ||
## RedisCluster ⇐ <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> | ||
* [RedisCluster](#RedisCluster) ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code> | ||
* [new RedisCluster(startupNodes, options)](#new_RedisCluster_new) | ||
* [.disconnect()](#RedisCluster#disconnect) | ||
* [.defineCommand(name, definition)](#Commander#defineCommand) | ||
<a name="new_RedisCluster_new"></a> | ||
### new RedisCluster(startupNodes, options) | ||
Creates a Redis 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>true</code> | See Redis class | | ||
| [options.refreshAfterFails] | <code>number</code> | <code>10</code> | When a MOVED error is returned, it's considered a failure. When the times of failures reach `refreshAfterFails`, client will call CLUSTER SLOTS command to refresh the slots. | | ||
<a name="RedisCluster#disconnect"></a> | ||
### redisCluster.disconnect() | ||
Disconnect from every node in the cluster. | ||
**Kind**: instance method of <code>[RedisCluster](#RedisCluster)</code> | ||
**Access:** public | ||
<a name="Commander#defineCommand"></a> | ||
### redisCluster.defineCommand(name, definition) | ||
Define a custom command using lua script | ||
**Kind**: instance method of <code>[RedisCluster](#RedisCluster)</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> | ||
@@ -226,1 +188,7 @@ ## Commander | ||
<a name="defaultOptions"></a> | ||
## defaultOptions | ||
Default options | ||
**Kind**: global variable | ||
**Access:** protected |
@@ -5,2 +5,10 @@ ## Changelog | ||
### v1.1.0 - May 1, 2015 | ||
* Support cluster auto reconnection. | ||
* Add `maxRedirections` option to Cluster. | ||
* Remove `roleRetryDelay` option in favor of `sentinelRetryStrategy`. | ||
* Improve compatibility with node_redis. | ||
* More stable sentinel connection. | ||
### v1.0.13 - April 27, 2015 | ||
@@ -7,0 +15,0 @@ |
@@ -1,1 +0,5 @@ | ||
module.exports = require('./lib/redis'); | ||
exports = module.exports = require('./lib/redis'); | ||
exports.ReplyError = require('./lib/reply_error'); | ||
exports.Promise = require('bluebird'); | ||
exports.Cluster = require('./lib/cluster'); |
@@ -8,2 +8,3 @@ 'use strict'; | ||
var commands = require('ioredis-commands'); | ||
/** | ||
@@ -65,58 +66,84 @@ * Command instance | ||
Command.prototype.getSlot = function () { | ||
if (typeof this._slot === 'undefined') { | ||
var key = this.getKeys()[0]; | ||
if (key) { | ||
this.slot = utils.calcSlot(key); | ||
} else { | ||
this.slot = null; | ||
} | ||
} | ||
return this.slot; | ||
}; | ||
Command.prototype.getKeys = function () { | ||
var keys = []; | ||
var i, keyStart, keyStop; | ||
var def = commands[this.name]; | ||
if (def) { | ||
switch (this.name) { | ||
case 'eval': | ||
case 'evalsha': | ||
keyStop = parseInt(this.args[1], 10) + 2; | ||
for (i = 2; i < keyStop; ++i) { | ||
keys.push(this.args[i]); | ||
} | ||
break; | ||
case 'sort': | ||
keys.push(this.args[0]); | ||
for (i = 1; i < this.args.length - 1; ++i) { | ||
if (typeof this.args[i] !== 'string') { | ||
continue; | ||
if (typeof this._keys === 'undefined') { | ||
this._keys = []; | ||
var i, keyStart, keyStop; | ||
var def = commands[this.name]; | ||
if (def) { | ||
switch (this.name) { | ||
case 'eval': | ||
case 'evalsha': | ||
keyStop = parseInt(this.args[1], 10) + 2; | ||
for (i = 2; i < keyStop; ++i) { | ||
this._keys.push(this.args[i]); | ||
} | ||
var directive = this.args[i].toUpperCase(); | ||
if (directive === 'GET') { | ||
i += 1; | ||
if (this.args[i] !== '#') { | ||
keys.push(this.getKeyPart(this.args[i])); | ||
break; | ||
case 'sort': | ||
this._keys.push(this.args[0]); | ||
for (i = 1; i < this.args.length - 1; ++i) { | ||
if (typeof this.args[i] !== 'string') { | ||
continue; | ||
} | ||
} else if (directive === 'BY') { | ||
i += 1; | ||
keys.push(this.getKeyPart(this.args[i])); | ||
} else if (directive === 'STORE') { | ||
i += 1; | ||
keys.push(this.args[i]); | ||
var directive = this.args[i].toUpperCase(); | ||
if (directive === 'GET') { | ||
i += 1; | ||
if (this.args[i] !== '#') { | ||
this._keys.push(this.getKeyPart(this.args[i])); | ||
} | ||
} else if (directive === 'BY') { | ||
i += 1; | ||
this._keys.push(this.getKeyPart(this.args[i])); | ||
} else if (directive === 'STORE') { | ||
i += 1; | ||
this._keys.push(this.args[i]); | ||
} | ||
} | ||
} | ||
break; | ||
case 'zunionstore': | ||
case 'zinterstore': | ||
keys.push(this.args[0]); | ||
keyStop = parseInt(this.args[1], 10) + 2; | ||
for (i = 2; i < keyStop; ++i) { | ||
keys.push(this.args[i]); | ||
} | ||
break; | ||
default: | ||
keyStart = def.keyStart - 1; | ||
keyStop = def.keyStop > 0 ? def.keyStop : this.args.length + def.keyStop + 1; | ||
if (keyStart >= 0 && keyStop <= this.args.length && keyStop > keyStart && def.step > 0) { | ||
for (i = keyStart; i < keyStop; i += def.step) { | ||
keys.push(this.args[i]); | ||
break; | ||
case 'zunionstore': | ||
case 'zinterstore': | ||
this._keys.push(this.args[0]); | ||
keyStop = parseInt(this.args[1], 10) + 2; | ||
for (i = 2; i < keyStop; ++i) { | ||
this._keys.push(this.args[i]); | ||
} | ||
break; | ||
default: | ||
keyStart = def.keyStart - 1; | ||
keyStop = def.keyStop > 0 ? def.keyStop : this.args.length + def.keyStop + 1; | ||
if (keyStart >= 0 && keyStop <= this.args.length && keyStop > keyStart && def.step > 0) { | ||
for (i = keyStart; i < keyStop; i += def.step) { | ||
this._keys.push(this.args[i]); | ||
} | ||
} | ||
break; | ||
} | ||
break; | ||
} | ||
} | ||
return keys; | ||
return this._keys; | ||
}; | ||
Command.prototype.getKeyPart = function (key) { | ||
var starPos = key.indexOf('*'); | ||
if (starPos === -1) { | ||
return key; | ||
} | ||
var hashPos = key.indexOf('->', starPos + 1); | ||
if (hashPos === 1) { | ||
return key; | ||
} | ||
return key.slice(0, hashPos); | ||
}; | ||
/** | ||
@@ -231,2 +258,37 @@ * Convert command to writable buffer or string | ||
Command.setArgumentTransformer('mset', function (args) { | ||
if (args.length === 1) { | ||
if (typeof Map !== 'undefined' && args[0] instanceof Map) { | ||
return utils.convertMapToArray(args[0]); | ||
} | ||
if ( typeof args[0] === 'object' && args[0] !== null) { | ||
return utils.convertObjectToArray(args[0]); | ||
} | ||
} | ||
return args; | ||
}); | ||
Command.setArgumentTransformer('hmset', function (args) { | ||
if (args.length === 2) { | ||
if (typeof Map !== 'undefined' && args[1] instanceof Map) { | ||
return [args[0]].concat(utils.convertMapToArray(args[1])); | ||
} | ||
if ( typeof args[1] === 'object' && args[1] !== null) { | ||
return [args[0]].concat(utils.convertObjectToArray(args[1])); | ||
} | ||
} | ||
return args; | ||
}); | ||
Command.setReplyTransformer('hgetall', function (result) { | ||
if (Array.isArray(result)) { | ||
var obj = {}; | ||
for (var i = 0; i < result.length; i += 2) { | ||
obj[result[i]] = result[i + 1]; | ||
} | ||
return obj; | ||
} | ||
return result; | ||
}); | ||
module.exports = Command; |
@@ -6,3 +6,3 @@ 'use strict'; | ||
var hiredis = require('hiredis'); | ||
var ReplyError = require('../redis').ReplyError; | ||
var ReplyError = require('../reply_error'); | ||
@@ -9,0 +9,0 @@ function HiredisReplyParser(options) { |
@@ -5,3 +5,4 @@ 'use strict'; | ||
var util = require('util'); | ||
var ReplyError = require('../redis').ReplyError; | ||
var utils = require('../utils'); | ||
var ReplyError = require('../reply_error'); | ||
@@ -26,11 +27,4 @@ function Packet(type, size) { | ||
function IncompleteReadBuffer(message) { | ||
Error.call(this); | ||
Error.captureStackTrace(this, this.constructor); | ||
var IncompleteReadBuffer = utils.extendsError('IncompleteReadBuffer'); | ||
this.name = this.constructor.name; | ||
this.message = message; | ||
} | ||
util.inherits(IncompleteReadBuffer, Error); | ||
// Buffer.toString() is quite slow for small strings | ||
@@ -37,0 +31,0 @@ function smallToString(buf, start, end) { |
@@ -110,3 +110,3 @@ 'use strict'; | ||
if (!--writePending) { | ||
_this.redis.connection.write(data); | ||
_this.redis.stream.write(data); | ||
} | ||
@@ -113,0 +113,0 @@ } |
299
lib/redis.js
@@ -6,5 +6,3 @@ 'use strict'; | ||
var EventEmitter = require('events').EventEmitter; | ||
var net = require('net'); | ||
var Promise = require('bluebird'); | ||
var url = require('url'); | ||
var Pipeline = require('./pipeline'); | ||
@@ -16,2 +14,10 @@ var Command = require('./command'); | ||
var debug = require('debug')('ioredis:redis'); | ||
var Connector = require('./connectors/connector'); | ||
var SentinelConnector = require('./connectors/sentinel_connector'); | ||
var JavaScriptParser = require('./parsers/javascript'); | ||
var NativeParser; | ||
try { | ||
NativeParser = require('./parsers/hiredis'); | ||
} catch (e) { | ||
} | ||
@@ -84,5 +90,5 @@ /** | ||
*/ | ||
function Redis(port, host, options) { | ||
function Redis() { | ||
if (!(this instanceof Redis)) { | ||
return new Redis(port, host, options); | ||
return new Redis(arguments[0], arguments[1], arguments[2]); | ||
} | ||
@@ -93,48 +99,8 @@ | ||
var resultOptions; | ||
if (typeof port === 'object') { | ||
// Redis(options) | ||
resultOptions = port; | ||
} else if (typeof port === 'string' && !utils.isInt(port)) { | ||
// Redis(url, options) | ||
var parsedOptions = {}; | ||
var parsedURL = url.parse(port, true, true); | ||
if (parsedURL.hostname) { | ||
parsedOptions.port = parsedURL.port; | ||
parsedOptions.host = parsedURL.hostname; | ||
if (parsedURL.auth) { | ||
parsedOptions.password = parsedURL.auth.split(':')[1]; | ||
} | ||
if (parsedURL.path) { | ||
parsedOptions.db = parseInt(parsedURL.path.slice(1), 10); | ||
} | ||
} else { | ||
parsedOptions.path = port; | ||
} | ||
resultOptions = _.defaults(host || {}, parsedOptions); | ||
} else { | ||
// Redis(port, host, options) or Redis(port, options) | ||
if (host && typeof host === 'object') { | ||
resultOptions = _.defaults(host, { port: port }); | ||
} else { | ||
resultOptions = _.defaults(options || {}, { port: port, host: host }); | ||
} | ||
} | ||
if (resultOptions && typeof resultOptions.port === 'string') { | ||
resultOptions.port = parseInt(resultOptions.port, 10); | ||
} | ||
this.parseOptions(arguments[0], arguments[1], arguments[2]); | ||
this.options = _.defaults(resultOptions || {}, this.options || {}, Redis._defaultOptions); | ||
if (this.options.parser === 'javascript') { | ||
this.Parser = require('./parsers/javascript'); | ||
this.Parser = JavaScriptParser; | ||
} else { | ||
try { | ||
this.Parser = require('./parsers/hiredis'); | ||
} catch (e) { | ||
if (this.options.parser === 'hiredis') { | ||
throw e; | ||
} | ||
this.Parser = require('./parsers/javascript'); | ||
} | ||
this.Parser = NativeParser || JavaScriptParser; | ||
} | ||
@@ -146,3 +112,5 @@ | ||
if (this.options.sentinels) { | ||
this._sentinel = new Sentinel(this.options.sentinels, this.options.role, this.options.name); | ||
this.connector = new SentinelConnector(this.options); | ||
} else { | ||
this.connector = new Connector(this.options); | ||
} | ||
@@ -152,7 +120,6 @@ | ||
// disconnected(or inactive) -> connected -> ready -> closing -> closed | ||
// end(or wait) -> connecting -> connect -> ready -> end | ||
if (this.options.lazyConnect) { | ||
this.status = 'inactive'; | ||
this.setStatus('wait'); | ||
} else { | ||
this.status = 'disconnected'; | ||
this.connect(); | ||
@@ -177,28 +144,66 @@ } | ||
* | ||
* @var _defaultOptions | ||
* @private | ||
* @var defaultOptions | ||
* @protected | ||
*/ | ||
Redis._defaultOptions = { | ||
Redis.defaultOptions = { | ||
// Connection | ||
port: 6379, | ||
host: 'localhost', | ||
family: 4, | ||
enableOfflineQueue: true, | ||
enableReadyCheck: true, | ||
connectTimeout: 3000, | ||
retryStrategy: function (times) { | ||
var delay = Math.min(times * 2, 2000); | ||
return delay; | ||
return Math.min(times * 2, 2000); | ||
}, | ||
autoResubscribe: true, | ||
parser: 'auto', | ||
lazyConnect: false, | ||
// Sentinel | ||
sentinels: null, | ||
name: null, | ||
role: 'master', | ||
sentinelRetryStrategy: function (times) { | ||
return Math.min(times * 10, 1000); | ||
}, | ||
// Status | ||
password: null, | ||
db: 0, | ||
role: 'master', | ||
sentinel: null, | ||
roleRetryDelay: 500, | ||
connectTimeout: 10000, | ||
name: null | ||
// Others | ||
parser: 'auto', | ||
enableOfflineQueue: true, | ||
enableReadyCheck: true, | ||
autoResubscribe: true, | ||
lazyConnect: false | ||
}; | ||
Redis.prototype.parseOptions = function () { | ||
this.options = {}; | ||
for (var i = 0; i < arguments.length; ++i) { | ||
var arg = arguments[i]; | ||
if (arg === null || typeof arg === 'undefined') { | ||
continue; | ||
} | ||
if (typeof arg === 'object') { | ||
_.defaults(this.options, arg); | ||
} else if (typeof arg === 'string') { | ||
_.defaults(this.options, utils.parseURL(arg)); | ||
} else if (typeof arg === 'number') { | ||
this.options.port = arg; | ||
} else { | ||
throw new Error('Invalid argument ' + arg); | ||
} | ||
} | ||
_.defaults(this.options, Redis.defaultOptions); | ||
if (typeof this.options.port === 'string') { | ||
this.options.port = parseInt(this.options.port, 10); | ||
} | ||
if (typeof this.options.db === 'string') { | ||
this.options.db = parseInt(this.options.db, 10); | ||
} | ||
}; | ||
Redis.prototype.setStatus = function (status) { | ||
debug('status[%s:%s]: %s -> %s', this.options.host, this.options.port, this.status || '[empty]', status); | ||
this.status = status; | ||
process.nextTick(this.emit.bind(this, status)); | ||
}; | ||
/** | ||
@@ -210,2 +215,7 @@ * Create a connection to Redis. | ||
Redis.prototype.connect = function () { | ||
if (this.status === 'connecting' || this.status === 'connect') { | ||
return false; | ||
} | ||
this.setStatus('connecting'); | ||
this.condition = { | ||
@@ -220,41 +230,28 @@ select: this.options.db, | ||
if (this._sentinel) { | ||
var _this = this; | ||
this._sentinel.removeAllListeners(); | ||
this._sentinel.once('connect', function (connection) { | ||
debug('received connection from sentinel'); | ||
_this.connection = connection; | ||
bindEvent(_this); | ||
}); | ||
this._sentinel.on('error', function (error) { | ||
_this.silentEmit('error', error); | ||
}); | ||
this._sentinel.connect(this.options.role); | ||
} else { | ||
var connectionOptions; | ||
if (this.options.path) { | ||
connectionOptions = _.pick(this.options, ['path']); | ||
} else { | ||
connectionOptions = _.pick(this.options, ['port', 'host', 'family']); | ||
var _this = this; | ||
this.connector.connect(function (err, stream) { | ||
if (err) { | ||
_this.flushQueue(err); | ||
return; | ||
} | ||
this.connection = net.createConnection(connectionOptions); | ||
bindEvent(this); | ||
} | ||
function bindEvent(self) { | ||
self.connection.once('connect', eventHandler.connectHandler(self)); | ||
self.connection.once('error', eventHandler.errorHandler(self)); | ||
self.connection.once('close', eventHandler.closeHandler(self)); | ||
self.connection.on('data', eventHandler.dataHandler(self)); | ||
if (self.options.connectTimeout) { | ||
self.connection.setTimeout(self.options.connectTimeout, function () { | ||
self.connection.setTimeout(0); | ||
self.manuallyClosing = true; | ||
self.connection.destroy(); | ||
_this.stream = stream; | ||
stream.once('connect', eventHandler.connectHandler(_this)); | ||
stream.once('error', eventHandler.errorHandler(_this)); | ||
stream.once('close', eventHandler.closeHandler(_this)); | ||
stream.on('data', eventHandler.dataHandler(_this)); | ||
if (_this.options.connectTimeout) { | ||
stream.setTimeout(_this.options.connectTimeout, function () { | ||
stream.setTimeout(0); | ||
_this.manuallyClosing = true; | ||
stream.destroy(); | ||
}); | ||
self.connection.once('connect', function () { | ||
self.connection.setTimeout(0); | ||
stream.once('connect', function () { | ||
stream.setTimeout(0); | ||
}); | ||
} | ||
} | ||
}); | ||
return true; | ||
}; | ||
@@ -266,16 +263,11 @@ | ||
* This method closes the connection immediately, | ||
* and may lose some pending replies that haven't written to clien. | ||
* and may lose some pending replies that haven't written to client. | ||
* If you want to wait for the pending replies, use Redis#quit instead. | ||
* @public | ||
*/ | ||
Redis.prototype.disconnect = function (options) { | ||
options = options || {}; | ||
this.manuallyClosing = !options.reconnect; | ||
if (this.connection) { | ||
this.connection.end(); | ||
Redis.prototype.disconnect = function (reconnect) { | ||
if (!reconnect) { | ||
this.manuallyClosing = true; | ||
} | ||
if (this._sentinel) { | ||
this._sentinel.disconnect(options); | ||
} | ||
this.connector.disconnect(); | ||
}; | ||
@@ -289,3 +281,3 @@ | ||
Redis.prototype.end = function () { | ||
this.disconnect.apply(this, arguments); | ||
this.disconnect(); | ||
}; | ||
@@ -493,18 +485,19 @@ | ||
Redis.prototype.sendCommand = function (command, stream) { | ||
if (this.status === 'inactive') { | ||
this.status = 'disconnected'; | ||
if (this.status === 'wait') { | ||
this.connect(); | ||
} | ||
if (this.status === 'end') { | ||
return command.reject(new Error('Connection is closed.')); | ||
} | ||
if (this.condition.mode.subscriber && !_.includes(Command.FLAGS.VALID_IN_SUBSCRIBER_MODE, command.name)) { | ||
command.reject(new Error('Connection in subscriber mode, only subscriber commands may be used')); | ||
return command.promise; | ||
return command.reject(new Error('Connection in subscriber mode, only subscriber commands may be used')); | ||
} | ||
var writable = (this.status === 'ready') || ((this.status === 'connected') && _.includes(Command.FLAGS.VALID_WHEN_LOADING, command.name)); | ||
var writable = (this.status === 'ready') || ((this.status === 'connect') && _.includes(Command.FLAGS.VALID_WHEN_LOADING, command.name)); | ||
if (!stream) { | ||
if (!this.connection) { | ||
if (!this.stream) { | ||
writable = false; | ||
} else if (!this.connection.writable) { | ||
} else if (!this.stream.writable) { | ||
writable = false; | ||
} else if (this.connection._writableState && this.connection._writableState.ended) { | ||
} else if (this.stream._writableState && this.stream._writableState.ended) { | ||
// https://github.com/iojs/io.js/pull/1217 | ||
@@ -515,5 +508,9 @@ writable = false; | ||
if (!writable && !this.options.enableOfflineQueue) { | ||
return command.reject(new Error('Stream isn\'t writeable and enableOfflineQueue options is false')); | ||
} | ||
if (writable) { | ||
debug('write command[%d] -> %s(%s)', this.condition.select, command.name, command.args); | ||
(stream || this.connection).write(command.toWritable()); | ||
(stream || this.stream).write(command.toWritable()); | ||
@@ -525,8 +522,2 @@ this.commandQueue.push(command); | ||
} | ||
if (command.name === 'select' && utils.isInt(command.args[0])) { | ||
this.condition.select = parseInt(command.args[0], 10); | ||
debug('switch to db [%d]', this.condition.select); | ||
} | ||
} else if (this.options.enableOfflineQueue) { | ||
@@ -539,10 +530,9 @@ debug('queue command[%d] -> %s(%s)', this.condition.select, command.name, command.args); | ||
}); | ||
if (command.name === 'select' && utils.isInt(command.args[0])) { | ||
this.condition.select = parseInt(command.args[0], 10); | ||
debug('switch to db [%d]', this.condition.select); | ||
} | ||
} else { | ||
command.reject(new Error('Stream isn\'t writeable and enableOfflineQueue options is false')); | ||
} | ||
if (command.name === 'select' && utils.isInt(command.args[0])) { | ||
this.condition.select = parseInt(command.args[0], 10); | ||
debug('switch to db [%d]', this.condition.select); | ||
} | ||
return command.promise; | ||
@@ -554,46 +544,1 @@ }; | ||
module.exports = Redis; | ||
Redis.Command = Command; | ||
Redis.Command.setArgumentTransformer('mset', function (args) { | ||
if (args.length === 1) { | ||
if (typeof Map !== 'undefined' && args[0] instanceof Map) { | ||
return utils.convertMapToArray(args[0]); | ||
} | ||
if ( typeof args[0] === 'object' && args[0] !== null) { | ||
return utils.convertObjectToArray(args[0]); | ||
} | ||
} | ||
return args; | ||
}); | ||
Redis.Command.setArgumentTransformer('hmset', function (args) { | ||
if (args.length === 2) { | ||
if (typeof Map !== 'undefined' && args[1] instanceof Map) { | ||
return [args[0]].concat(utils.convertMapToArray(args[1])); | ||
} | ||
if ( typeof args[1] === 'object' && args[1] !== null) { | ||
return [args[0]].concat(utils.convertObjectToArray(args[1])); | ||
} | ||
} | ||
return args; | ||
}); | ||
Redis.Command.setReplyTransformer('hgetall', function (result) { | ||
if (Array.isArray(result)) { | ||
var obj = {}; | ||
for (var i = 0; i < result.length; i += 2) { | ||
obj[result[i]] = result[i + 1]; | ||
} | ||
return obj; | ||
} | ||
return result; | ||
}); | ||
Redis.Cluster = require('./redis_cluster'); | ||
Redis.ReplyError = require('./reply_error'); | ||
Redis.Promise = Promise; | ||
var Sentinel = require('./sentinel'); |
@@ -7,5 +7,3 @@ 'use strict'; | ||
return function () { | ||
debug('status: %s -> connected', self.status); | ||
self.status = 'connected'; | ||
self.emit('connect'); | ||
self.setStatus('connect'); | ||
@@ -17,11 +15,4 @@ self.commandQueue = []; | ||
self.auth(self.condition.auth, function (err, res) { | ||
if (res === 'OK') { | ||
return; | ||
} | ||
if (err.message.match('no password is set')) { | ||
if (err && err.message.match('no password is set')) { | ||
console.warn('`auth` is specified in the client but not in the server.'); | ||
} else if (err) { | ||
self.silentEmit('error', new Error('Auth error: ' + err.message)); | ||
} else { | ||
self.silentEmit('error', new Error('Auth failed: ' + res)); | ||
} | ||
@@ -35,4 +26,3 @@ }); | ||
// TODO cross file calling private method | ||
self._initParser(); | ||
self.initParser(); | ||
@@ -43,15 +33,10 @@ if (self.options.enableReadyCheck) { | ||
self.flushQueue(new Error('Ready check failed: ' + err.message)); | ||
return; | ||
} else { | ||
self.serverInfo = info; | ||
if (self.connector.check(info)) { | ||
exports.readyHandler(self)(); | ||
} else { | ||
self.disconnect(true); | ||
} | ||
} | ||
if (self._sentinel && info.role && self.options.role !== info.role) { | ||
debug('role invalid, expected %s, but got %s', self.options.role, info.role); | ||
self.disconnect(); | ||
setTimeout(function () { | ||
self.connect(); | ||
}, self.options.roleRetryDelay); | ||
return; | ||
} | ||
self.serverInfo = info; | ||
exports.readyHandler(self)(); | ||
}); | ||
@@ -64,3 +49,4 @@ } | ||
return function () { | ||
debug('status: %s -> closed', self.status); | ||
self.setStatus('end'); | ||
self.prevCondition = self.condition; | ||
@@ -87,6 +73,4 @@ | ||
self.setStatus('reconnecting'); | ||
setTimeout(function () { | ||
self.status = 'reconnecting'; | ||
self.emit('reconnecting'); | ||
self.connect(); | ||
@@ -98,4 +82,2 @@ }, retryDelay); | ||
self.flushQueue(new Error('Connection is closed.')); | ||
self.status = 'closed'; | ||
self.emit('close'); | ||
} | ||
@@ -119,4 +101,3 @@ }; | ||
return function () { | ||
debug('status: %s -> ready', self.status); | ||
self.status = 'ready'; | ||
self.setStatus('ready'); | ||
self.retryAttempts = 0; | ||
@@ -168,4 +149,3 @@ | ||
} | ||
self.emit('ready'); | ||
}; | ||
}; |
@@ -15,3 +15,3 @@ 'use strict'; | ||
*/ | ||
exports._initParser = function () { | ||
exports.initParser = function () { | ||
var self = this; | ||
@@ -154,3 +154,3 @@ | ||
if (--command.remainReplies === 0) { | ||
command.resolve(reply[2]); | ||
command.resolve(count); | ||
return true; | ||
@@ -157,0 +157,0 @@ } |
'use strict'; | ||
var util = require('util'); | ||
var utils = require('./utils'); | ||
function ReplyError(message) { | ||
Error.call(this); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.name = this.constructor.name; | ||
this.message = message; | ||
} | ||
// inherit from Error | ||
util.inherits(ReplyError, Error); | ||
module.exports = ReplyError; | ||
module.exports = utils.extendsError('ReplyError'); |
'use strict'; | ||
var urllib = require('url'); | ||
var util = require('util'); | ||
var _ = require('lodash'); | ||
@@ -250,1 +253,49 @@ /** | ||
}; | ||
exports.parseURL = function (url) { | ||
if (exports.isInt(url)) { | ||
return { port: url }; | ||
} | ||
var parsed = urllib.parse(url, true, true); | ||
if (!parsed.slashes && url[0] !== '/') { | ||
url = '//' + url; | ||
parsed = urllib.parse(url, true, true); | ||
} | ||
var result = {}; | ||
if (parsed.auth) { | ||
result.password = parsed.auth.split(':')[1]; | ||
} | ||
if (parsed.pathname) { | ||
if (parsed.protocol === 'redis:') { | ||
result.db = parsed.pathname.slice(1); | ||
} else { | ||
result.path = parsed.pathname; | ||
} | ||
} | ||
if (parsed.host) { | ||
result.host = parsed.hostname; | ||
} | ||
if (parsed.port) { | ||
result.port = parsed.port; | ||
} | ||
_.defaults(result, parsed.query); | ||
return result; | ||
}; | ||
exports.extendsError = function (name) { | ||
var errorClass = function (message) { | ||
Error.call(this); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.name = name; | ||
this.message = message; | ||
}; | ||
// inherit from Error | ||
util.inherits(errorClass, Error); | ||
return errorClass; | ||
}; |
{ | ||
"name": "ioredis", | ||
"version": "1.0.13", | ||
"version": "1.1.0", | ||
"description": "A delightful, performance-focused Redis client for Node and io.js", | ||
@@ -10,3 +10,3 @@ "main": "index.js", | ||
"generate-docs": "jsdoc2md lib/redis.js lib/redis_cluster.js lib/commander.js > API.md", | ||
"bench": "matcha benchmark.js" | ||
"bench": "matcha benchmarks/*.js" | ||
}, | ||
@@ -19,6 +19,5 @@ "repository": { | ||
"redis", | ||
"node", | ||
"io", | ||
"cluster", | ||
"sentinel" | ||
"sentinel", | ||
"pipelining" | ||
], | ||
@@ -25,0 +24,0 @@ "author": "luin <i@zihua.li> (http://zihua.li)", |
@@ -5,2 +5,3 @@ # ioredis | ||
[![Test Coverage](https://codeclimate.com/github/luin/ioredis/badges/coverage.svg)](https://codeclimate.com/github/luin/ioredis) | ||
[![Code Climate](https://codeclimate.com/github/luin/ioredis/badges/gpa.svg)](https://codeclimate.com/github/luin/ioredis) | ||
[![Dependency Status](https://david-dm.org/luin/ioredis.svg)](https://david-dm.org/luin/ioredis) | ||
@@ -90,3 +91,4 @@ [![Join the chat at https://gitter.im/luin/ioredis](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/luin/ioredis?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
Here is a simple example of the API for publish / subscribe. | ||
This program opens two client connections. It subscribes to a channel with one connection, | ||
The following program opens two client connections. | ||
It subscribes to a channel with one connection, | ||
and publishes to that channel with the other: | ||
@@ -119,2 +121,10 @@ | ||
`PSUBSCRIBE` is also supported in a similar way: | ||
```javascript | ||
redis.psubscribe('pat?ern', function (err, count) {}); | ||
redis.on('pmessage', function (pattern, channel, message) {}); | ||
redis.on('pmessageBuffer', function (pattern, channel, message) {}); | ||
``` | ||
When a client issues a SUBSCRIBE or PSUBSCRIBE, that connection is put into a "subscriber" mode. | ||
@@ -377,2 +387,15 @@ At that point, only commands that modify the subscription set are valid. | ||
## Connection Events | ||
Redis instance will emit some events about the state of the connection to the Redis server. | ||
### "connect" | ||
client will emit `connect` once a connection is established to the Redis server. | ||
### "ready" | ||
If `enableReadyCheck` is `true`, client will emit `ready` when the server reports that it is ready to receive commands. | ||
Otherwise `ready` will be emitted immediately right after the `connect` event. | ||
### "end" | ||
client will emit `end` when an established Redis server connection has closed. | ||
## Offline Queue | ||
@@ -412,8 +435,15 @@ When a command can't be processed by Redis(e.g. the connection hasn't been established or | ||
Besides `retryStrategy` option, there's also a `sentinelRetryStrategy` in Sentinel mode which will be invoked when all the sentinel nodes are unreachable during connecting. If `sentinelRetryStrategy` returns a valid delay time, ioredis will try to reconnect from scratch. The default value of `sentinelRetryStrategy` is: | ||
```javascript | ||
function (times) { | ||
var delay = Math.min(times * 10, 1000); | ||
return delay; | ||
} | ||
``` | ||
## Cluster | ||
Support for Cluster is currently experimental and under active development. It's not recommended to use it in production. | ||
If you encounter any problems, welcome to submit an issue :-). | ||
Redis Cluster provides a way to run a Redis installation where data is automatically sharded across multiple Redis nodes. | ||
You can connect to a Redis Cluster like this: | ||
You can connect to a cluster like this: | ||
```javascript | ||
@@ -435,9 +465,29 @@ var Redis = require('ioredis'); | ||
``` | ||
When using `Redis.Cluster` to connect to a cluster, there are some differences from using `Redis`: | ||
0. The argument is a list of nodes of the cluster you want to connect. | ||
`Cluster` constructor accepts two arguments, where: | ||
0. The first argument is a list of nodes of the cluster you want to connect to. | ||
Just like Sentinel, the list does not need to enumerate all your cluster nodes, | ||
but a few so that if one is down the client will try the next one, and the client will discover other nodes automatically when at least one node is connnected. | ||
0. Pipelining is not available in the cluster currently. | ||
but a few so that if one is unreachable the client will try the next one, and the client will discover other nodes automatically when at least one node is connnected. | ||
0. The second argument is the option that will be passed to the `Redis` constructor when creating connections to Redis nodes internally. There are some additional options: | ||
0. `clusterRetryStrategy`: When none of the startup nodes are reachable, `clusterRetryStrategy` will be invoked. When a number is returned, | ||
ioredis will try to reconnect the startup nodes from scratch after the specified delay(ms). Otherwise an error of "None of startup nodes is available" will returned. | ||
The default value of this option is: | ||
```javascript | ||
function (times) { | ||
var delay = Math.min(100 + times * 2, 2000); | ||
return delay; | ||
} | ||
``` | ||
0. `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`. | ||
0. `maxRedirections`: When a `MOVED` or `ASK` error is received, client will redirect the | ||
command to another node. This option limits the max redirections allowed when sending a command. The default value is `16`. | ||
0. `retryDelayOnFailover`: When the error of "Connection is closed." is received when sending a command, | ||
ioredis will retry after the specified delay. The default value is `2000`. You should make sure to let `retryDelayOnFailover * maxRedirections > cluster-node-timeout` | ||
in order to insure that no command will fails during a failover. | ||
Currently pipeline isn't supported in the Cluster mode. | ||
## hiredis | ||
@@ -554,3 +604,3 @@ If [hiredis](https://github.com/redis/hiredis-node) is installed(by `npm install hiredis`), | ||
You can find the code at `benchmark.js` and run it yourself using `npm run bench`. | ||
You can find the code at `benchmarks/*.js` and run it yourself using `npm run bench`. | ||
@@ -557,0 +607,0 @@ # Running tests |
123
test.js
@@ -8,2 +8,73 @@ 'use strict'; | ||
var Redis = require('./'); | ||
// var redis = new Redis(); | ||
// redis.disconnect(); | ||
// setTimeout(function () { | ||
// redis.get('foo', function () { | ||
// console.log(arguments); | ||
// }); | ||
// }, 1000); | ||
// setTimeout(function () { | ||
// }, 100000); | ||
// redis.on('error', function () { | ||
// console.log('error', arguments); | ||
// }); | ||
// redis.on('timeout', function () { | ||
// console.log('timeout', arguments); | ||
// }); | ||
// redis.on('close', function () { | ||
// console.log('close', arguments); | ||
// }); | ||
// redis.on('connect', function () { | ||
// console.log('connect', arguments); | ||
// }); | ||
// var redis = new Redis(); | ||
var redis = new Redis({ | ||
sentinels: [ | ||
{ host: 'localhost', port: 26379 } | ||
], | ||
name: 'master1' | ||
}); | ||
redis.set('foo', 'bar'); | ||
setInterval(function () { | ||
redis.get('foo', console.log); | ||
}, 1000); | ||
// setTimeout(function () { | ||
// console.log('start'); | ||
// var pending = 0; | ||
// for (var i = 0; i < 3000; ++i) { | ||
// pending += 1; | ||
// doo(function () { | ||
// if (!--pending) { | ||
// console.log('done'); | ||
// } | ||
// }); | ||
// } | ||
// }, 10000); | ||
// function doo(callback) { | ||
// var pending = 0; | ||
// for (var j = 0; j < 50; ++j) { | ||
// pending += 1; | ||
// redis.set('foo', 'bar', function () { | ||
// if (!--pending) { | ||
// callback(); | ||
// } | ||
// }); | ||
// } | ||
// } | ||
// setInterval(function () { | ||
// redis.get('foo', function () { | ||
// console.log(arguments); | ||
// }); | ||
// }, 2000); | ||
// var redis = new Redis({ showFriendlyErrorStack: 10 }); | ||
@@ -53,24 +124,40 @@ // var redis = new Redis(); | ||
var cluster = new Redis.Cluster([{ | ||
port: 6380, | ||
host: '127.0.0.1' | ||
}, { | ||
port: 6381, | ||
host: '127.0.0.1' | ||
}]); | ||
// var cluster = new Redis.Cluster([{ | ||
// port: 6380, | ||
// host: '127.0.0.1' | ||
// }, { | ||
// port: 6381, | ||
// host: '127.0.0.1' | ||
// }], { showFriendlyErrorStack: true }); | ||
cluster.defineCommand('testGet', { | ||
numberOfKeys: 1, | ||
lua: 'return redis.call("mget", KEYS[1], ARGV[1])' | ||
}); | ||
cluster.testGet('foo', '{fo}2', function (err, res) { | ||
console.log(err, res); | ||
}); | ||
// var redis = new Redis(6388, '177.22.22.2', { | ||
// enableReadyCheck: false | ||
// setTimeout(function () { | ||
// // cluster.disconnect(); | ||
// }, 2000); | ||
// cluster.get('foo3', function (err, res) { | ||
// console.log(err, res); | ||
// }); | ||
// redis.on('end', function (err) { | ||
// console.log(err) | ||
// cluster.defineCommand('testGet', { | ||
// numberOfKeys: 1, | ||
// lua: 'return redis.call("mget", KEYS[1], ARGV[1])' | ||
// }); | ||
// cluster.set('foo', 'bar'); | ||
// cluster.set('foo2', 'bar1'); | ||
// cluster.set('{foo}2', 'bar2'); | ||
// setInterval(function () { | ||
// cluster.testGet('foo', '{foo}2', console.log); | ||
// }, 1000); | ||
// // cluster.testGet('foo', '{fo}2', function (err, res) { | ||
// // console.log(err, res); | ||
// // }); | ||
// // var redis = new Redis(6388, '177.22.22.2', { | ||
// // enableReadyCheck: false | ||
// // }); | ||
// // redis.on('end', function (err) { | ||
// // console.log(err) | ||
// // }); | ||
// redis.disconnect(); | ||
@@ -77,0 +164,0 @@ // var redis = new Redis({ |
@@ -43,2 +43,17 @@ 'use strict'; | ||
}); | ||
it('should warn when the server doesn\'t need auth', function (done) { | ||
stub(console, 'warn', function () { | ||
console.warn.restore(); | ||
redis.disconnect(); | ||
server.disconnect(); | ||
done(); | ||
}); | ||
var server = new MockServer(17379, function (argv) { | ||
if (argv[0] === 'auth' && argv[1] === 'pass') { | ||
return new Error('ERR Client sent AUTH, but no password is set'); | ||
} | ||
}); | ||
var redis = new Redis({ port: 17379, password: 'pass' }); | ||
}); | ||
}); |
@@ -7,2 +7,36 @@ 'use strict'; | ||
describe('connect', function () { | ||
it('should flush the queue when all startup nodes are unreachable', function (done) { | ||
var cluster = new Redis.Cluster([ | ||
{ host: '127.0.0.1', port: '30001' } | ||
], { clusterRetryStrategy: null }); | ||
cluster.get('foo', function (err) { | ||
expect(err.message).to.match(/None of startup nodes is available/); | ||
cluster.disconnect(); | ||
done(); | ||
}); | ||
}); | ||
it('should invoke clusterRetryStrategy when all startup nodes are unreachable', function (done) { | ||
var t = 0; | ||
var cluster = new Redis.Cluster([ | ||
{ host: '127.0.0.1', port: '30001' }, | ||
{ host: '127.0.0.1', port: '30002' } | ||
], { | ||
clusterRetryStrategy: function (times) { | ||
expect(times).to.eql(++t); | ||
if (times === 3) { | ||
return; | ||
} | ||
return 0; | ||
} | ||
}); | ||
cluster.get('foo', function (err) { | ||
expect(t).to.eql(3); | ||
expect(err.message).to.match(/None of startup nodes is available/); | ||
done(); | ||
}); | ||
}); | ||
it('should connect to cluster successfully', function (done) { | ||
@@ -51,3 +85,3 @@ var node = new MockServer(30001); | ||
it('should send command the the correct node', function (done) { | ||
it('should send command to the correct node', function (done) { | ||
var node1 = new MockServer(30001, function (argv) { | ||
@@ -63,4 +97,6 @@ if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
process.nextTick(function () { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
}); | ||
} | ||
@@ -89,4 +125,6 @@ }); | ||
expect(moved).to.eql(true); | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
process.nextTick(function () { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
}); | ||
} | ||
@@ -131,4 +169,6 @@ } | ||
if (++times === 2) { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
process.nextTick(function () { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
}); | ||
} else { | ||
@@ -148,2 +188,86 @@ return new Error('ASK ' + utils.calcSlot('foo') + ' 127.0.0.1:30001'); | ||
}); | ||
describe('maxRedirections', function () { | ||
it('should return error when reached max redirection', function (done) { | ||
var redirectTimes = 0; | ||
var argvHandler = 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') { | ||
redirectTimes += 1; | ||
return new Error('ASK ' + utils.calcSlot('foo') + ' 127.0.0.1:30001'); | ||
} | ||
}; | ||
var node1 = new MockServer(30001, argvHandler); | ||
var node2 = new MockServer(30002, argvHandler); | ||
var cluster = new Redis.Cluster([ | ||
{ host: '127.0.0.1', port: '30001' } | ||
], { maxRedirections: 5 }); | ||
cluster.get('foo', function (err) { | ||
expect(redirectTimes).to.eql(5); | ||
expect(err.message).to.match(/Too many Cluster redirections/); | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
}); | ||
}); | ||
}); | ||
describe('refreshAfterFails', function () { | ||
it('should re-fetch slots when reached refreshAfterFails', function (done) { | ||
var redirectTimes = 0; | ||
var argvHandler = function (argv) { | ||
if (argv[0] === 'cluster' && argv[1] === 'slots') { | ||
if (redirectTimes === 4) { | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
} | ||
return [ | ||
[0, 1, ['127.0.0.1', 30001]], | ||
[2, 16383, ['127.0.0.1', 30002]] | ||
]; | ||
} else if (argv[0] === 'get' && argv[1] === 'foo') { | ||
if (redirectTimes < 4) { | ||
redirectTimes += 1; | ||
return new Error('MOVED ' + utils.calcSlot('foo') + ' 127.0.0.1:30001'); | ||
} | ||
} | ||
}; | ||
var node1 = new MockServer(30001, argvHandler); | ||
var node2 = new MockServer(30002, argvHandler); | ||
var cluster = new Redis.Cluster([ | ||
{ host: '127.0.0.1', port: '30001' } | ||
], { refreshAfterFails: 4 }); | ||
cluster.get('foo'); | ||
}); | ||
}); | ||
it('should get value successfully', function (done) { | ||
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]] | ||
]; | ||
} | ||
}); | ||
var node2 = new MockServer(30002, function (argv) { | ||
if (argv[0] === 'get' && argv[1] === 'foo') { | ||
return 'bar'; | ||
} | ||
}); | ||
var cluster = new Redis.Cluster([ | ||
{ host: '127.0.0.1', port: '30001' } | ||
]); | ||
cluster.get('foo', function (err, result) { | ||
expect(result).to.eql('bar'); | ||
cluster.disconnect(); | ||
disconnect([node1, node2], done); | ||
}); | ||
}); | ||
}); | ||
@@ -150,0 +274,0 @@ |
@@ -14,4 +14,6 @@ 'use strict'; | ||
var redis = new Redis(); | ||
redis.once('close', done); | ||
redis.disconnect(); | ||
redis.once('end', done); | ||
redis.once('connect', function () { | ||
redis.disconnect(); | ||
}); | ||
}); | ||
@@ -37,3 +39,3 @@ | ||
redis.set('foo', 'bar', function () { | ||
redis.connection.end(); | ||
redis.stream.end(); | ||
}); | ||
@@ -56,8 +58,40 @@ redis.get('foo', function (err, res) { | ||
var redis = new Redis({ connectTimeout: 10000 }); | ||
stub(redis.connection, 'setTimeout', function (timeout) { | ||
expect(timeout).to.eql(0); | ||
redis.connection.setTimeout.restore(); | ||
done(); | ||
setImmediate(function () { | ||
stub(redis.stream, 'setTimeout', function (timeout) { | ||
expect(timeout).to.eql(0); | ||
redis.stream.setTimeout.restore(); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('retryStrategy', function () { | ||
it('should pass the correct retry times', function (done) { | ||
var t = 0; | ||
new Redis({ | ||
port: 1, | ||
retryStrategy: function (times) { | ||
expect(times).to.eql(++t); | ||
if (times === 3) { | ||
done(); | ||
return; | ||
} | ||
return 0; | ||
} | ||
}); | ||
}); | ||
it('should skip reconnecting when retryStrategy doesn\'t return a number', function (done) { | ||
var redis = new Redis({ | ||
port: 1, | ||
retryStrategy: function () { | ||
process.nextTick(function () { | ||
expect(redis.status).to.eql('end'); | ||
done(); | ||
}); | ||
return null; | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -135,3 +135,3 @@ 'use strict'; | ||
it('should able to send quit command in the subscriber mode', function (done) { | ||
it('should be able to send quit command in the subscriber mode', function (done) { | ||
var redis = new Redis(); | ||
@@ -144,3 +144,3 @@ var pending = 1; | ||
}); | ||
redis.on('close', function () { | ||
redis.on('end', function () { | ||
expect(pending).to.eql(0); | ||
@@ -152,3 +152,3 @@ redis.disconnect(); | ||
it('should restore subscription after reconnecting', function (done) { | ||
it('should restore subscription after reconnecting(subscribe)', function (done) { | ||
var redis = new Redis(); | ||
@@ -172,2 +172,22 @@ var pub = new Redis(); | ||
}); | ||
it('should restore subscription after reconnecting(psubscribe)', function (done) { | ||
var redis = new Redis(); | ||
var pub = new Redis(); | ||
redis.psubscribe('fo?o', 'ba?r', function () { | ||
redis.on('ready', function () { | ||
var pending = 2; | ||
redis.on('pmessage', function (pattern, channel, message) { | ||
if (!--pending) { | ||
redis.disconnect(); | ||
pub.disconnect(); | ||
done(); | ||
} | ||
}); | ||
pub.publish('fo1o', 'hi1'); | ||
pub.publish('ba1r', 'hi2'); | ||
}); | ||
redis.disconnect({ reconnect: true }); | ||
}); | ||
}); | ||
}); |
@@ -18,3 +18,3 @@ 'use strict'; | ||
redis.set('foo', '2', function () { | ||
redis.connection.destroy(); | ||
redis.stream.destroy(); | ||
redis.select('3'); | ||
@@ -42,3 +42,3 @@ redis.set('foo', '3'); | ||
redis.set('foo', '2', function () { | ||
redis.connection.destroy(); | ||
redis.stream.destroy(); | ||
redis.get('foo', function (err, res) { | ||
@@ -45,0 +45,0 @@ expect(res).to.eql('2'); |
@@ -37,3 +37,4 @@ 'use strict'; | ||
it('should raise error when all sentinel are unreachable', function (done) { | ||
it('should call sentinelRetryStrategy when all sentinels are unreachable', function (done) { | ||
var t = 0; | ||
var redis = new Redis({ | ||
@@ -44,13 +45,16 @@ sentinels: [ | ||
], | ||
sentinelRetryStrategy: function (times) { | ||
expect(times).to.eql(++t); | ||
var sentinel = new MockServer(27380); | ||
sentinel.once('connect', function () { | ||
redis.disconnect(); | ||
sentinel.disconnect(done); | ||
}); | ||
return 0; | ||
}, | ||
name: 'master' | ||
}); | ||
redis.once('error', function (error) { | ||
redis.disconnect(); | ||
expect(error.message).to.match(/are unreachable/); | ||
done(); | ||
}); | ||
}); | ||
it('should continue trying when all sentinels are unreachable', function (done) { | ||
it('should raise error when all sentinel are unreachable and retry is disabled', function (done) { | ||
var redis = new Redis({ | ||
@@ -61,15 +65,14 @@ sentinels: [ | ||
], | ||
sentinelRetryStrategy: null, | ||
name: 'master' | ||
}); | ||
redis.once('error', function (err) { | ||
var sentinel = new MockServer(27380); | ||
sentinel.once('connect', function () { | ||
redis.disconnect(); | ||
sentinel.disconnect(done); | ||
}); | ||
redis.get('foo', function (error) { | ||
expect(error.message).to.match(/are unreachable/); | ||
redis.disconnect(); | ||
done(); | ||
}); | ||
}); | ||
it('should also close the connect to the sentinel when disconnect', function (done) { | ||
it('should close the connection to the sentinel when resolving successfully', function (done) { | ||
var sentinel = new MockServer(27379, function (argv) { | ||
@@ -82,2 +85,3 @@ if (argv[0] === 'sentinel' && argv[1] === 'get-master-addr-by-name') { | ||
sentinel.once('disconnect', function () { | ||
redis.disconnect(); | ||
master.disconnect(function () { | ||
@@ -94,3 +98,2 @@ sentinel.disconnect(done); | ||
}); | ||
redis.disconnect(); | ||
}); | ||
@@ -174,4 +177,3 @@ }); | ||
], | ||
name: 'master', | ||
roleRetryDelay: 0 | ||
name: 'master' | ||
}); | ||
@@ -259,4 +261,3 @@ }); | ||
name: 'master', | ||
role: 'slave', | ||
roleRetryDelay: 0 | ||
role: 'slave' | ||
}); | ||
@@ -263,0 +264,0 @@ }); |
@@ -0,1 +1,3 @@ | ||
'use strict'; | ||
var net = require('net'); | ||
@@ -13,3 +15,5 @@ var util = require('util'); | ||
this.socket = net.createServer(function (c) { | ||
_this.emit('connect', c); | ||
process.nextTick(function () { | ||
_this.emit('connect', c); | ||
}); | ||
@@ -16,0 +20,0 @@ var parser = new Parser({ returnBuffer: false }); |
@@ -54,2 +54,24 @@ 'use strict'; | ||
}); | ||
describe('#getKeys()', function () { | ||
it('should return keys', function () { | ||
expect(getKeys('get', ['foo'])).to.eql(['foo']); | ||
expect(getKeys('mget', ['foo', 'bar'])).to.eql(['foo', 'bar']); | ||
expect(getKeys('mset', ['foo', 'v1', 'bar', 'v2'])).to.eql(['foo', 'bar']); | ||
expect(getKeys('hmset', ['key', 'foo', 'v1', 'bar', 'v2'])).to.eql(['key']); | ||
expect(getKeys('blpop', ['key1', 'key2', '17'])).to.eql(['key1', 'key2']); | ||
expect(getKeys('evalsha', ['23123', '2', 'foo', 'bar', 'zoo'])).to.eql(['foo', 'bar']); | ||
expect(getKeys('evalsha', ['23123', 2, 'foo', 'bar', 'zoo'])).to.eql(['foo', 'bar']); | ||
expect(getKeys('sort', ['key'])).to.eql(['key']); | ||
expect(getKeys('sort', ['key', 'BY', 'hash:*->field'])).to.eql(['key', 'hash:*']); | ||
expect(getKeys('sort', ['key', 'BY', 'hash:*->field', 'LIMIT', 2, 3, 'GET', 'gk', 'GET', '#', 'Get', 'gh->f*', 'DESC', 'ALPHA', 'STORE', 'store'])).to.eql(['key', 'hash:*', 'gk', 'gh->f', 'store']); | ||
expect(getKeys('zunionstore', ['out', 2, 'zset1', 'zset2', 'WEIGHTS', 2, 3])).to.eql(['out', 'zset1', 'zset2']); | ||
expect(getKeys('zinterstore', ['out', 2, 'zset1', 'zset2', 'WEIGHTS', 2, 3])).to.eql(['out', 'zset1', 'zset2']); | ||
function getKeys(commandName, args) { | ||
var command = new Command(commandName, args); | ||
return command.getKeys(); | ||
} | ||
}); | ||
}); | ||
}); |
@@ -126,2 +126,27 @@ 'use strict'; | ||
}); | ||
describe('.parseURL', function () { | ||
it('should return correctly', function () { | ||
expect(utils.parseURL('/tmp.sock')).to.eql({ path: '/tmp.sock' }); | ||
expect(utils.parseURL('127.0.0.1')).to.eql({ host: '127.0.0.1' }); | ||
expect(utils.parseURL('6379')).to.eql({ port: '6379' }); | ||
expect(utils.parseURL('127.0.0.1:6379')).to.eql({ | ||
host: '127.0.0.1', | ||
port: '6379' | ||
}); | ||
expect(utils.parseURL('127.0.0.1:6379?db=2&key=value')).to.eql({ | ||
host: '127.0.0.1', | ||
port: '6379', | ||
db: '2', | ||
key: 'value' | ||
}); | ||
expect(utils.parseURL('redis://user:pass@127.0.0.1:6380/4?key=value')).to.eql({ | ||
host: '127.0.0.1', | ||
port: '6380', | ||
db: '4', | ||
password: 'pass', | ||
key: 'value' | ||
}); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
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
199525
61
4566
659