You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

572 lines
17 KiB

'use strict';
var _ = require('lodash');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Promise = require('bluebird');
var Deque = require('double-ended-queue');
var Command = require('./command');
var Commander = require('./commander');
var utils = require('./utils');
var eventHandler = require('./redis/event_handler');
var debug = require('debug')('ioredis:redis');
var Connector = require('./connectors/connector');
var SentinelConnector = require('./connectors/sentinel_connector');
var ScanStream = require('./scan_stream');
var JavaScriptParser = require('./parsers/javascript');
var NativeParser;
try {
NativeParser = require('./parsers/hiredis');
} catch (e) { /* I'm sorry, eslint */}
/**
* Creates a Redis instance
*
* @constructor
* @param {(number|string|Object)} [port=6379] - Port of the Redis server,
* or a URL string(see the examples below),
* or the `options` object(see the third argument).
* @param {string|Object} [host=localhost] - Host of the Redis server,
* when the first argument is a URL string,
* this argument is an object represents the options.
* @param {Object} [options] - Other options.
* @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 {string} [options.path=null] - Local domain socket path. If set the `port`,
* `host` and `family` will be ignored.
* @param {number} [options.keepAlive=0] - TCP KeepAlive on the socket with a X ms delay before start.
* @param {string} [options.connectionName=null] - Connection name.
* Set to a non-number value to disable keepAlive.
* @param {number} [options.db=0] - Database index to use.
* @param {string} [options.password=null] - If set, client will send AUTH command
* with the value of this option when connected.
* @param {boolean} [options.enableReadyCheck=true] - When a connection is established to
* the Redis server, the server might still be loading the database from disk.
* While loading, the server not respond to any commands.
* To work around this, when this option is `true`,
* ioredis will check the status of the Redis server,
* and when the Redis server is able to process commands,
* a `ready` event will be emitted.
* @param {boolean} [options.enableOfflineQueue=true] - By default,
* if there is no active connection to the Redis server,
* commands are added to a queue and are executed once the connection is "ready"
* (when `enableReadyCheck` is `true`,
* "ready" means the Redis server has loaded the database from disk, otherwise means the connection
* to the Redis server has been established). If this option is false,
* when execute the command when the connection isn't ready, an error will be returned.
* @param {number} [options.connectTimeout=10000] - The milliseconds before a timeout occurs during the initial
* connection to the Redis server.
* @param {boolean} [options.autoResubscribe=true] - After reconnected, if the previous connection was in the
* subscriber mode, client will auto re-subscribe these channels.
* @param {boolean} [options.autoResendUnfulfilledCommands=true] - If true, client will resend unfulfilled
* commands(e.g. block commands) in the previous connection when reconnected.
* @param {boolean} [options.lazyConnect=false] - By default,
* When a new `Redis` instance is created, it will connect to Redis server automatically.
* If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to
* the constructor:
* @param {string} [options.keyPrefix=''] - The prefix to prepend to all keys in a command.
* ```javascript
* var redis = new Redis({ lazyConnect: true });
* // No attempting to connect to the Redis server here.
* // Now let's connect to the Redis server
* redis.get('foo', function () {
* });
* ```
* @param {function} [options.retryStrategy] - See "Quick Start" section
* @param {function} [options.reconnectOnError] - See "Quick Start" section
* @extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
* @extends Commander
* @example
* ```js
* var Redis = require('ioredis');
*
* var redis = new Redis();
* // or: var redis = Redis();
*
* var redisOnPort6380 = new Redis(6380);
* var anotherRedis = new Redis(6380, '192.168.100.1');
* var unixSocketRedis = new Redis({ path: '/tmp/echo.sock' });
* var unixSocketRedis2 = new Redis('/tmp/echo.sock');
* var urlRedis = new Redis('redis://user:password@redis-service.com:6379/');
* var urlRedis2 = new Redis('//localhost:6379');
* var authedRedis = new Redis(6380, '192.168.100.1', { password: 'password' });
* ```
*/
function Redis() {
if (!(this instanceof Redis)) {
return new Redis(arguments[0], arguments[1], arguments[2]);
}
EventEmitter.call(this);
Commander.call(this);
this.parseOptions(arguments[0], arguments[1], arguments[2]);
if (this.options.parser === 'javascript') {
this.Parser = JavaScriptParser;
} else {
this.Parser = NativeParser || JavaScriptParser;
}
this.resetCommandQueue();
this.resetOfflineQueue();
if (this.options.sentinels) {
this.connector = new SentinelConnector(this.options);
} else {
this.connector = new Connector(this.options);
}
this.retryAttempts = 0;
// end(or wait) -> connecting -> connect -> ready -> end
if (this.options.lazyConnect) {
this.setStatus('wait');
} else {
this.connect().catch(function () {});
}
}
util.inherits(Redis, EventEmitter);
_.assign(Redis.prototype, Commander.prototype);
/**
* Create a Redis instance
*
* @deprecated
*/
Redis.createClient = function () {
return Redis.apply(this, arguments);
};
/**
* Default options
*
* @var defaultOptions
* @protected
*/
Redis.defaultOptions = {
// Connection
port: 6379,
host: 'localhost',
family: 4,
connectTimeout: 3000,
retryStrategy: function (times) {
return Math.min(times * 2, 2000);
},
keepAlive: 0,
connectionName: null,
// Sentinel
sentinels: null,
name: null,
role: 'master',
sentinelRetryStrategy: function (times) {
return Math.min(times * 10, 1000);
},
// Status
password: null,
db: 0,
// Others
parser: 'auto',
enableOfflineQueue: true,
enableReadyCheck: true,
autoResubscribe: true,
autoResendUnfulfilledCommands: true,
lazyConnect: false,
keyPrefix: '',
reconnectOnError: null
};
Redis.prototype.resetCommandQueue = function () {
this.commandQueue = new Deque();
};
Redis.prototype.resetOfflineQueue = function () {
this.offlineQueue = new Deque();
};
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);
}
};
/**
* Change instance's status
* @private
*/
Redis.prototype.setStatus = function (status, arg) {
var address;
if (this.options.path) {
address = this.options.path;
} else if (this.stream && this.stream.remoteAddress && this.stream.remotePort) {
address = this.stream.remoteAddress + ':' + this.stream.remotePort;
} else {
address = this.options.host + ':' + this.options.port;
}
debug('status[%s]: %s -> %s', address, this.status || '[empty]', status);
this.status = status;
process.nextTick(this.emit.bind(this, status, arg));
};
/**
* Create a connection to Redis.
* This method will be invoked automatically when creating a new Redis instance.
* @param {function} callback
* @return {Promise}
* @public
*/
Redis.prototype.connect = function (callback) {
return new Promise(function (resolve, reject) {
if (this.status === 'connecting' || this.status === 'connect' || this.status === 'ready') {
reject(new Error('Redis is already connecting/connected'));
return;
}
this.setStatus('connecting');
this.condition = {
select: this.options.db,
auth: this.options.password,
subscriber: false
};
var _this = this;
this.connector.connect(function (err, stream) {
if (err) {
_this.flushQueue(err);
_this.silentEmit('error', err);
reject(err);
return;
}
var CONNECT_EVENT = _this.options.tls ? 'secureConnect' : 'connect';
_this.stream = stream;
if (typeof _this.options.keepAlive === 'number') {
stream.setKeepAlive(true, _this.options.keepAlive);
}
stream.once(CONNECT_EVENT, 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);
stream.destroy();
});
stream.once(CONNECT_EVENT, function () {
stream.setTimeout(0);
});
}
var connectionConnectHandler = function () {
_this.removeListener('close', connectionCloseHandler);
resolve();
};
var connectionCloseHandler = function (err) {
_this.removeListener(CONNECT_EVENT, connectionConnectHandler);
reject(err);
};
_this.once(CONNECT_EVENT, connectionConnectHandler);
_this.once('close', connectionCloseHandler);
});
}.bind(this)).nodeify(callback);
};
/**
* Disconnect from Redis.
*
* This method closes the connection immediately,
* 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 (reconnect) {
if (!reconnect) {
this.manuallyClosing = true;
}
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
this.connector.disconnect();
};
/**
* Disconnect from Redis.
*
* @deprecated
*/
Redis.prototype.end = function () {
this.disconnect();
};
/**
* Create a new instance with the same options as the current one.
*
* @example
* ```js
* var redis = new Redis(6380);
* var anotherRedis = redis.duplicate();
* ```
*
* @public
*/
Redis.prototype.duplicate = function (override) {
return new Redis(_.assign(_.cloneDeep(this.options), override || {}));
};
/**
* Flush offline queue and command queue with error.
*
* @param {Error} error - The error object to send to the commands
* @private
*/
Redis.prototype.flushQueue = function (error) {
var item;
while (this.offlineQueue.length > 0) {
item = this.offlineQueue.shift();
item.command.reject(error);
}
while (this.commandQueue.length > 0) {
item = this.commandQueue.shift();
item.command.reject(error);
}
};
/**
* Check whether Redis has finished loading the persistent data and is able to
* process commands.
*
* @param {Function} callback
* @private
*/
Redis.prototype._readyCheck = function (callback) {
var _this = this;
this.info(function (err, res) {
if (err) {
return callback(err);
}
if (typeof res !== 'string') {
return callback(null, res);
}
var info = {};
var lines = res.split('\r\n');
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].split(':');
if (parts[1]) {
info[parts[0]] = parts[1];
}
}
if (!info.loading || info.loading === '0') {
callback(null, info);
} else {
var retryTime = (info.loading_eta_seconds || 1) * 1000;
debug('Redis server still loading, trying again in ' + retryTime + 'ms');
setTimeout(function () {
_this._readyCheck(callback);
}, retryTime);
}
});
};
/**
* Emit only when there's at least one listener.
*
* @param {string} eventName - Event to emit
* @param {...*} arguments - Arguments
* @return {boolean} Returns true if event had listeners, false otherwise.
* @private
*/
Redis.prototype.silentEmit = function (eventName) {
if (this.listeners(eventName).length > 0) {
return this.emit.apply(this, arguments);
}
return false;
};
/**
* Listen for all requests received by the server in real time.
*
* This command will create a new connection to Redis and send a
* MONITOR command via the new connection in order to avoid disturbing
* the current connection.
*
* @param {function} [callback] The callback function. If omit, a promise will be returned.
* @example
* ```js
* var redis = new Redis();
* redis.monitor(function (err, monitor) {
* // Entering monitoring mode.
* monitor.on('monitor', function (time, args) {
* console.log(time + ": " + util.inspect(args));
* });
* });
*
* // supports promise as well as other commands
* redis.monitor().then(function (monitor) {
* monitor.on('monitor', function (time, args) {
* console.log(time + ": " + util.inspect(args));
* });
* });
* ```
* @public
*/
Redis.prototype.monitor = function (callback) {
var monitorInstance = this.duplicate({
monitor: true,
lazyConnect: false
});
return new Promise(function (resolve) {
monitorInstance.once('monitoring', function () {
resolve(monitorInstance);
});
}).nodeify(callback);
};
require('./transaction').addTransactionSupport(Redis.prototype);
/**
* Send a command to Redis
*
* This method is used internally by the `Redis#set`, `Redis#lpush` etc.
* Most of the time you won't invoke this method directly.
* However when you want to send a command that is not supported by ioredis yet,
* this command will be useful.
*
* @method sendCommand
* @memberOf Redis#
* @param {Command} command - The Command instance to send.
* @see {@link Command}
* @example
* ```js
* var redis = new Redis();
*
* // Use callback
* var get = new Command('get', ['foo'], 'utf8', function (err, result) {
* console.log(result);
* });
* redis.sendCommand(get);
*
* // Use promise
* var set = new Command('set', ['foo', 'bar'], 'utf8');
* set.promise.then(function (result) {
* console.log(result);
* });
* redis.sendCommand(set);
* ```
* @private
*/
Redis.prototype.sendCommand = function (command, stream) {
if (this.status === 'wait') {
this.connect().catch(function () {});
}
if (this.status === 'end') {
command.reject(new Error('Connection is closed.'));
return command.promise;
}
if (this.condition.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;
}
var writable = (this.status === 'ready') ||
((this.status === 'connect') && _.includes(Command.FLAGS.VALID_WHEN_LOADING, command.name));
if (!this.stream) {
writable = false;
} else if (!this.stream.writable) {
writable = false;
} else if (this.stream._writableState && this.stream._writableState.ended) {
// https://github.com/iojs/io.js/pull/1217
writable = false;
}
if (!writable && !this.options.enableOfflineQueue) {
command.reject(new Error('Stream isn\'t writeable and enableOfflineQueue options is false'));
return command.promise;
}
if (writable) {
debug('write command[%d] -> %s(%s)', this.condition.select, command.name, command.args);
(stream || this.stream).write(command.toWritable());
this.commandQueue.push({
command: command,
stream: stream,
select: this.condition.select
});
if (_.includes(Command.FLAGS.WILL_DISCONNECT, command.name)) {
this.manuallyClosing = true;
}
} else if (this.options.enableOfflineQueue) {
debug('queue command[%d] -> %s(%s)', this.condition.select, command.name, command.args);
this.offlineQueue.push({
command: command,
stream: stream,
select: this.condition.select
});
}
if (command.name === 'select' && utils.isInt(command.args[0])) {
var db = parseInt(command.args[0], 10);
if (this.condition.select !== db) {
this.condition.select = db;
this.emit('select', db);
debug('switch to db [%d]', this.condition.select);
}
}
return command.promise;
};
['scan', 'sscan', 'hscan', 'zscan', 'scanBuffer', 'sscanBuffer', 'hscanBuffer', 'zscanBuffer']
.forEach(function (command) {
Redis.prototype[command + 'Stream'] = function (key, options) {
if (command === 'scan' || command === 'scanBuffer') {
options = key;
key = null;
}
return new ScanStream(_.defaults({
objectMode: true,
key: key,
redis: this,
command: command
}, options));
};
});
_.assign(Redis.prototype, require('./redis/parser'));
module.exports = Redis;