1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

Improve error handling

Added individual error classes
Don't silently fail for commands without callback from now on
General polishing (e.g. better error messages)

Fix typos
This commit is contained in:
Ruben Bridgewater
2016-04-25 01:36:49 +02:00
parent bf394923fd
commit 03f1a606f7
18 changed files with 367 additions and 129 deletions

View File

@@ -137,6 +137,7 @@ are passed an object containing `delay` (in ms) and `attempt` (the attempt #) at
### "error" ### "error"
`client` will emit `error` when encountering an error connecting to the Redis server or when any other in node_redis occurs. `client` will emit `error` when encountering an error connecting to the Redis server or when any other in node_redis occurs.
If you use a command without callback and encounter a ReplyError it is going to be emitted to the error listener.
So please attach the error listener to node_redis. So please attach the error listener to node_redis.
@@ -293,6 +294,51 @@ client.get("foo_rand000000000000", function (err, reply) {
`client.end()` without the flush parameter set to true should NOT be used in production! `client.end()` without the flush parameter set to true should NOT be used in production!
## Error handling (>= v.2.6)
All redis errors are returned as `ReplyError`.
All unresolved commands that get rejected due to what ever reason return a `AbortError`.
As subclass of the `AbortError` a `AggregateError` exists. This is emitted in case multiple unresolved commands without callback got rejected in debug_mode.
They are all aggregated and a single error is emitted in that case.
Example:
```js
var redis = require('./');
var assert = require('assert');
var client = redis.createClient();
client.on('error', function (err) {
assert(err instanceof Error);
assert(err instanceof redis.AbortError);
assert(err instanceof redis.AggregateError);
assert.strictEqual(err.errors.length, 2); // The set and get got aggregated in here
assert.strictEqual(err.code, 'NR_CLOSED');
});
client.set('foo', 123, 'bar', function (err, res) { // To many arguments
assert(err instanceof redis.ReplyError); // => true
assert.strictEqual(err.command, 'SET');
assert.deepStrictEqual(err.args, ['foo', 123, 'bar']);
redis.debug_mode = true;
client.set('foo', 'bar');
client.get('foo');
process.nextTick(function () {
client.end(true); // Force closing the connection while the command did not yet return
redis.debug_mode = false;
});
});
```
Every `ReplyError` contains the `command` name in all-caps and the arguments (`args`).
If node_redis emits a library error because of another error, the triggering error is added to the returned error as `origin` attribute.
___Error codes___
node_redis returns a `NR_CLOSED` error code if the clients connection dropped. If a command unresolved command got rejected a `UNERCTAIN_STATE` code is returned.
A `CONNECTION_BROKEN` error code is used in case node_redis gives up to reconnect.
## client.unref() ## client.unref()
Call `unref()` on the underlying socket connection to the Redis server, allowing the program to exit once no more commands are pending. Call `unref()` on the underlying socket connection to the Redis server, allowing the program to exit once no more commands are pending.
@@ -537,7 +583,7 @@ Redis. The interface in `node_redis` is to return an individual `Batch` object b
The only difference between .batch and .multi is that no transaction is going to be used. The only difference between .batch and .multi is that no transaction is going to be used.
Be aware that the errors are - just like in multi statements - in the result. Otherwise both, errors and results could be returned at the same time. Be aware that the errors are - just like in multi statements - in the result. Otherwise both, errors and results could be returned at the same time.
If you fire many commands at once this is going to **boost the execution speed by up to 400%** [sic!] compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired. If you fire many commands at once this is going to boost the execution speed significantly compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired.
## Monitor mode ## Monitor mode

164
index.js
View File

@@ -5,7 +5,7 @@ var tls = require('tls');
var util = require('util'); var util = require('util');
var utils = require('./lib/utils'); var utils = require('./lib/utils');
var Queue = require('double-ended-queue'); var Queue = require('double-ended-queue');
var CommandError = require('./lib/customError'); var errorClasses = require('./lib/customErrors');
var Command = require('./lib/command').Command; var Command = require('./lib/command').Command;
var OfflineCommand = require('./lib/command').OfflineCommand; var OfflineCommand = require('./lib/command').OfflineCommand;
var EventEmitter = require('events'); var EventEmitter = require('events');
@@ -189,13 +189,24 @@ function create_parser (self) {
self.return_reply(data); self.return_reply(data);
}, },
returnError: function (err) { returnError: function (err) {
self.return_error(err); // Return a ReplyError to indicate Redis returned an error
self.return_error(new errorClasses.ReplyError(err));
}, },
returnFatalError: function (err) { returnFatalError: function (err) {
// Error out all fired commands. Otherwise they might rely on faulty data. We have to reconnect to get in a working state again // Error out all fired commands. Otherwise they might rely on faulty data. We have to reconnect to get in a working state again
self.flush_and_error(err, ['command_queue']); // Note: the execution order is important. First flush and emit, then create the stream
self.stream.destroy(); err = new errorClasses.ReplyError(err);
self.return_error(err); err.message += '. Please report this.';
self.ready = false;
self.flush_and_error({
message: 'Fatal error encountert. Command aborted.',
code: 'NR_FATAL'
}, {
error: err,
queues: ['command_queue']
});
self.emit('error', err);
self.create_stream();
}, },
returnBuffers: self.buffers || self.message_buffers, returnBuffers: self.buffers || self.message_buffers,
name: self.options.parser, name: self.options.parser,
@@ -240,7 +251,7 @@ RedisClient.prototype.create_stream = function () {
this.stream.setTimeout(this.connect_timeout, function () { this.stream.setTimeout(this.connect_timeout, function () {
// Note: This is only tested if a internet connection is established // Note: This is only tested if a internet connection is established
self.retry_totaltime = self.connect_timeout; self.retry_totaltime = self.connect_timeout;
self.connection_gone('timeout', new Error('Redis connection gone from timeout event')); self.connection_gone('timeout');
}); });
} }
@@ -270,11 +281,11 @@ RedisClient.prototype.create_stream = function () {
}); });
this.stream.once('close', function (hadError) { this.stream.once('close', function (hadError) {
self.connection_gone('close', hadError ? new Error('Stream connection closed with a transmission error') : null); self.connection_gone('close');
}); });
this.stream.once('end', function () { this.stream.once('end', function () {
self.connection_gone('end', null); self.connection_gone('end');
}); });
this.stream.on('drain', function () { this.stream.on('drain', function () {
@@ -325,30 +336,46 @@ RedisClient.prototype.warn = function (msg) {
}; };
// Flush provided queues, erroring any items with a callback first // Flush provided queues, erroring any items with a callback first
RedisClient.prototype.flush_and_error = function (error, queue_names) { RedisClient.prototype.flush_and_error = function (error_attributes, options) {
var callbacks_not_called = []; options = options || {};
queue_names = queue_names || ['offline_queue', 'command_queue']; var aggregated_errors = [];
var queue_names = options.queues || ['command_queue', 'offline_queue']; // Flush the command_queue first to keep the order intakt
for (var i = 0; i < queue_names.length; i++) { for (var i = 0; i < queue_names.length; i++) {
// If the command was fired it might have been processed so far
if (queue_names[i] === 'command_queue') {
error_attributes.message += ' It might have been processed.';
} else { // As the command_queue is flushed first, remove this for the offline queue
error_attributes.message = error_attributes.message.replace(' It might have been processed.', '');
}
// Don't flush everything from the queue
for (var command_obj = this[queue_names[i]].shift(); command_obj; command_obj = this[queue_names[i]].shift()) { for (var command_obj = this[queue_names[i]].shift(); command_obj; command_obj = this[queue_names[i]].shift()) {
var err = new CommandError(error); var err = new errorClasses.AbortError(error_attributes);
err.command = command_obj.command.toUpperCase(); err.command = command_obj.command.toUpperCase();
if (command_obj.args.length) { if (command_obj.args && command_obj.args.length) {
err.args = command_obj.args; err.args = command_obj.args;
} }
if (options.error) {
err.origin = options.error;
}
if (typeof command_obj.callback === 'function') { if (typeof command_obj.callback === 'function') {
command_obj.callback(err); command_obj.callback(err);
} else { } else {
callbacks_not_called.push(err); aggregated_errors.push(err);
} }
} }
this[queue_names[i]] = new Queue();
} }
// Mutate the original error that will be emitted // Currently this would be a breaking change, therefore it's only emitted in debug_mode
// This is fine, as we don't manipulate any user errors if (exports.debug_mode && aggregated_errors.length) {
if (callbacks_not_called.length !== 0) { var error;
error.errors = callbacks_not_called; if (aggregated_errors.length === 1) {
error = aggregated_errors[0];
} else {
error_attributes.message = error_attributes.message.replace('It', 'They').replace(/command/i, '$&s');
error = new errorClasses.AggregateError(error_attributes);
error.errors = aggregated_errors;
}
this.emit('error', error);
} }
return callbacks_not_called.length === 0;
}; };
RedisClient.prototype.on_error = function (err) { RedisClient.prototype.on_error = function (err) {
@@ -538,6 +565,7 @@ RedisClient.prototype.connection_gone = function (why, error) {
if (this.retry_timer) { if (this.retry_timer) {
return; return;
} }
error = error || null;
debug('Redis connection is gone from ' + why + ' event.'); debug('Redis connection is gone from ' + why + ' event.');
this.connected = false; this.connected = false;
@@ -564,9 +592,12 @@ RedisClient.prototype.connection_gone = function (why, error) {
// If this is a requested shutdown, then don't retry // If this is a requested shutdown, then don't retry
if (this.closing) { if (this.closing) {
debug('Connection ended by quit / end command, not retrying.'); debug('Connection ended by quit / end command, not retrying.');
error = new Error('Stream connection ended and running command aborted. It might have been processed.'); this.flush_and_error({
error.code = 'NR_OFFLINE'; message: 'Stream connection ended and command aborted.',
this.flush_and_error(error); code: 'NR_CLOSED'
}, {
error: error
});
return; return;
} }
@@ -586,31 +617,39 @@ RedisClient.prototype.connection_gone = function (why, error) {
if (typeof this.retry_delay !== 'number') { if (typeof this.retry_delay !== 'number') {
// Pass individual error through // Pass individual error through
if (this.retry_delay instanceof Error) { if (this.retry_delay instanceof Error) {
error = new CommandError(this.retry_delay); error = this.retry_delay;
}
// Attention: there might be the case where there's no error!
if (!error) {
error = new Error('Stream connection ended and running command aborted. It might have been processed.');
error.code = 'NR_OFFLINE';
}
// Only emit an error in case that a running command had no callback
if (!this.flush_and_error(error)) {
error.message = 'Stream connection ended and all running commands aborted. They might have been processed.';
this.emit('error', error);
} }
this.flush_and_error({
message: 'Stream connection ended and command aborted.',
code: 'NR_CLOSED'
}, {
error: error
});
this.end(false); this.end(false);
return; return;
} }
} }
if (this.max_attempts !== 0 && this.attempts >= this.max_attempts || this.retry_totaltime >= this.connect_timeout) { if (this.max_attempts !== 0 && this.attempts >= this.max_attempts || this.retry_totaltime >= this.connect_timeout) {
var message = this.retry_totaltime >= this.connect_timeout ? var message = 'Redis connection in broken state: ';
'connection timeout exceeded.' : if (this.retry_totaltime >= this.connect_timeout) {
'maximum connection attempts exceeded.'; message += 'connection timeout exceeded.';
error = new Error('Redis connection in broken state: ' + message); } else {
error.code = 'CONNECTION_BROKEN'; message += 'maximum connection attempts exceeded.';
this.flush_and_error(error); }
this.emit('error', error);
this.flush_and_error({
message: message,
code: 'CONNECTION_BROKEN',
}, {
error: error
});
var err = new Error(message);
err.code = 'CONNECTION_BROKEN';
if (error) {
err.origin = error;
}
this.emit('error', err);
this.end(false); this.end(false);
return; return;
} }
@@ -620,13 +659,13 @@ RedisClient.prototype.connection_gone = function (why, error) {
this.offline_queue.unshift.apply(this.offline_queue, this.command_queue.toArray()); this.offline_queue.unshift.apply(this.offline_queue, this.command_queue.toArray());
this.command_queue.clear(); this.command_queue.clear();
} else if (this.command_queue.length !== 0) { } else if (this.command_queue.length !== 0) {
error = new Error('Redis connection lost and command aborted in uncertain state. It might have been processed.'); this.flush_and_error({
error.code = 'UNCERTAIN_STATE'; message: 'Redis connection lost and command aborted.',
if (!this.flush_and_error(error, ['command_queue'])) { code: 'UNCERTAIN_STATE'
// Only emit if not all commands had a callback that already handled the error }, {
error.message = 'Redis connection lost and commands aborted in uncertain state. They might have been processed.'; error: error,
this.emit('error', error); queues: ['command_queue']
} });
} }
if (this.retry_max_delay !== null && this.retry_delay > this.retry_max_delay) { if (this.retry_max_delay !== null && this.retry_delay > this.retry_max_delay) {
@@ -643,11 +682,9 @@ RedisClient.prototype.connection_gone = function (why, error) {
RedisClient.prototype.return_error = function (err) { RedisClient.prototype.return_error = function (err) {
var command_obj = this.command_queue.shift(); var command_obj = this.command_queue.shift();
if (command_obj && command_obj.command && command_obj.command.toUpperCase) { err.command = command_obj.command.toUpperCase();
err.command = command_obj.command.toUpperCase(); if (command_obj.args && command_obj.args.length) {
if (command_obj.args.length) { err.args = command_obj.args;
err.args = command_obj.args;
}
} }
// Count down pub sub mode if in entering modus // Count down pub sub mode if in entering modus
@@ -661,7 +698,7 @@ RedisClient.prototype.return_error = function (err) {
err.code = match[1]; err.code = match[1];
} }
utils.callback_or_emit(this, command_obj && command_obj.callback, err); utils.callback_or_emit(this, command_obj.callback, err);
}; };
RedisClient.prototype.drain = function () { RedisClient.prototype.drain = function () {
@@ -809,14 +846,16 @@ function handle_offline_command (self, command_obj) {
msg = 'Stream not writeable.'; msg = 'Stream not writeable.';
} }
} else { } else {
msg = 'The connection has already been closed.'; msg = 'The connection is already closed.';
} }
err = new Error(command + " can't be processed. " + msg); err = new errorClasses.AbortError({
err.command = command; message: command + " can't be processed. " + msg,
if (command_obj.args.length) { code: 'NR_CLOSED',
command: command
});
if (command_obj.args && command_obj.args.length) {
err.args = command_obj.args; err.args = command_obj.args;
} }
err.code = 'NR_OFFLINE';
utils.reply_in_order(self, callback, err); utils.reply_in_order(self, callback, err);
} else { } else {
debug('Queueing ' + command + ' for next server connection.'); debug('Queueing ' + command + ' for next server connection.');
@@ -889,8 +928,8 @@ RedisClient.prototype.internal_send_command = function (command, args, callback,
args_copy[i] = '' + args[i]; args_copy[i] = '' + args[i];
} }
} }
args = null; // Pass the original args to make sure in error cases the original arguments are returned
command_obj = new Command(command, args_copy, buffer_args, callback); command_obj = new Command(command, args, buffer_args, callback);
if (this.options.prefix) { if (this.options.prefix) {
prefix_keys = commands.getKeyIndexes(command, args_copy); prefix_keys = commands.getKeyIndexes(command, args_copy);
@@ -1053,6 +1092,9 @@ exports.createClient = function () {
exports.RedisClient = RedisClient; exports.RedisClient = RedisClient;
exports.print = utils.print; exports.print = utils.print;
exports.Multi = require('./lib/multi'); exports.Multi = require('./lib/multi');
exports.AbortError = errorClasses.AbortError;
exports.ReplyError = errorClasses.ReplyError;
exports.AggregateError = errorClasses.AggregateError;
// Add all redis commands / node_redis api to the client // Add all redis commands / node_redis api to the client
require('./lib/individualCommands'); require('./lib/individualCommands');

View File

@@ -1,16 +0,0 @@
'use strict';
var util = require('util');
function CommandError (error) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = error.message;
for (var keys = Object.keys(error), key = keys.pop(); key; key = keys.pop()) {
this[key] = error[key];
}
}
util.inherits(CommandError, Error);
module.exports = CommandError;

78
lib/customErrors.js Normal file
View File

@@ -0,0 +1,78 @@
'use strict';
var util = require('util');
function AbortError (obj) {
Error.captureStackTrace(this, this.constructor);
var message;
Object.defineProperty(this, 'name', {
get: function () {
return this.constructor.name;
}
});
Object.defineProperty(this, 'message', {
get: function () {
return message;
},
set: function (msg) {
message = msg;
}
});
for (var keys = Object.keys(obj), key = keys.pop(); key; key = keys.pop()) {
this[key] = obj[key];
}
// Explicitly add the message
// If the obj is a error itself, the message is not enumerable
this.message = obj.message;
}
function ReplyError (obj) {
Error.captureStackTrace(this, this.constructor);
var tmp;
Object.defineProperty(this, 'name', {
get: function () {
return this.constructor.name;
}
});
Object.defineProperty(this, 'message', {
get: function () {
return tmp;
},
set: function (msg) {
tmp = msg;
}
});
this.message = obj.message;
}
function AggregateError (obj) {
Error.captureStackTrace(this, this.constructor);
var tmp;
Object.defineProperty(this, 'name', {
get: function () {
return this.constructor.name;
}
});
Object.defineProperty(this, 'message', {
get: function () {
return tmp;
},
set: function (msg) {
tmp = msg;
}
});
for (var keys = Object.keys(obj), key = keys.pop(); key; key = keys.pop()) {
this[key] = obj[key];
}
this.message = obj.message;
}
util.inherits(ReplyError, Error);
util.inherits(AbortError, Error);
util.inherits(AggregateError, AbortError);
module.exports = {
ReplyError: ReplyError,
AbortError: AbortError,
AggregateError: AggregateError
};

View File

@@ -47,10 +47,10 @@ RedisClient.prototype.send_command = RedisClient.prototype.sendCommand = functio
RedisClient.prototype.end = function (flush) { RedisClient.prototype.end = function (flush) {
// Flush queue if wanted // Flush queue if wanted
if (flush) { if (flush) {
var err = new Error("The command can't be processed. The connection has already been closed."); this.flush_and_error({
err.code = 'NR_OFFLINE'; message: 'Connection forcefully ended and command aborted.',
this.flush_and_error(err); code: 'NR_CLOSED'
// TODO: Emit an error in case a command did not have a callback });
} else if (arguments.length === 0) { } else if (arguments.length === 0) {
this.warn( this.warn(
'Using .end() without the flush parameter is deprecated and throws from v.3.0.0 on.\n' + 'Using .end() without the flush parameter is deprecated and throws from v.3.0.0 on.\n' +

View File

@@ -79,7 +79,7 @@ Multi.prototype.monitor = Multi.prototype.MONITOR = function monitor (callback)
function quit_callback (self, callback) { function quit_callback (self, callback) {
return function (err, res) { return function (err, res) {
if (err && err.code === 'NR_OFFLINE') { if (err && err.code === 'NR_CLOSED') {
// Pretent the quit command worked properly in this case. // Pretent the quit command worked properly in this case.
// Either the quit landed in the offline queue and was flushed at the reconnect // Either the quit landed in the offline queue and was flushed at the reconnect
// or the offline queue is deactivated and the command was rejected right away // or the offline queue is deactivated and the command was rejected right away
@@ -90,7 +90,7 @@ function quit_callback (self, callback) {
} }
utils.callback_or_emit(self, callback, err, res); utils.callback_or_emit(self, callback, err, res);
if (self.stream.writable) { if (self.stream.writable) {
// If the socket is still alive, kill it. This could happen if quit got a NR_OFFLINE error code // If the socket is still alive, kill it. This could happen if quit got a NR_CLOSED error code
self.stream.destroy(); self.stream.destroy();
} }
}; };

View File

@@ -36,7 +36,7 @@ describe("The 'batch' method", function () {
batch.set('foo', 'bar'); batch.set('foo', 'bar');
batch.exec(function (err, res) { batch.exec(function (err, res) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
assert.strictEqual(res[0].code, 'NR_OFFLINE'); assert.strictEqual(res[0].code, 'NR_CLOSED');
done(); done();
}); });
}); });

View File

@@ -31,7 +31,7 @@ describe("The 'dbsize' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
client.dbsize([], function (err, res) { client.dbsize([], function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
}); });

View File

@@ -31,7 +31,7 @@ describe("The 'flushdb' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
client.flushdb(function (err, res) { client.flushdb(function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
}); });

View File

@@ -31,14 +31,14 @@ describe("The 'get' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
client.get(key, function (err, res) { client.get(key, function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
}); });
it('reports an error promisified', function () { it('reports an error promisified', function () {
return client.getAsync(key).then(assert, function (err) { return client.getAsync(key).then(assert, function (err) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
}); });
}); });
}); });

View File

@@ -32,7 +32,7 @@ describe("The 'getset' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
client.get(key, function (err, res) { client.get(key, function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
}); });

View File

@@ -24,8 +24,10 @@ describe("The 'hgetall' method", function () {
it('handles simple keys and values', function (done) { it('handles simple keys and values', function (done) {
client.hmset(['hosts', '__proto__', '1', 'another', '23', 'home', '1234'], helper.isString('OK')); client.hmset(['hosts', '__proto__', '1', 'another', '23', 'home', '1234'], helper.isString('OK'));
client.HGETALL(['hosts'], function (err, obj) { client.HGETALL(['hosts'], function (err, obj) {
assert.strictEqual(3, Object.keys(obj).length); if (!/^v0\.10/.test(process.version)) {
assert.strictEqual('1', obj.__proto__.toString()); // eslint-disable-line no-proto assert.strictEqual(3, Object.keys(obj).length);
assert.strictEqual('1', obj.__proto__.toString()); // eslint-disable-line no-proto
}
assert.strictEqual('23', obj.another.toString()); assert.strictEqual('23', obj.another.toString());
assert.strictEqual('1234', obj.home.toString()); assert.strictEqual('1234', obj.home.toString());
done(err); done(err);

View File

@@ -33,7 +33,7 @@ describe("The 'mset' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
client.mset(key, value, key2, value2, function (err, res) { client.mset(key, value, key2, value2, function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
}); });
@@ -96,7 +96,8 @@ describe("The 'mset' method", function () {
// this behavior is different from the 'set' behavior. // this behavior is different from the 'set' behavior.
it('emits an error', function (done) { it('emits an error', function (done) {
client.on('error', function (err) { client.on('error', function (err) {
assert.equal(err.message, "ERR wrong number of arguments for 'mset' command"); assert.strictEqual(err.message, "ERR wrong number of arguments for 'mset' command");
assert.strictEqual(err.name, 'ReplyError');
done(); done();
}); });

View File

@@ -23,7 +23,7 @@ describe("The 'select' method", function () {
it('returns an error if redis is not connected', function (done) { it('returns an error if redis is not connected', function (done) {
var buffering = client.select(1, function (err, res) { var buffering = client.select(1, function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
assert(typeof buffering === 'boolean'); assert(typeof buffering === 'boolean');

View File

@@ -31,7 +31,7 @@ describe("The 'set' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
client.set(key, value, function (err, res) { client.set(key, value, function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
}); });

View File

@@ -53,7 +53,7 @@ describe('connection tests', function () {
} }
}); });
client.set('foo', 'bar', function (err, res) { client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, 'Stream connection ended and running command aborted. It might have been processed.'); assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
called = -1; called = -1;
}); });
}); });
@@ -62,7 +62,7 @@ describe('connection tests', function () {
var called = false; var called = false;
client = redis.createClient(9999); client = redis.createClient(9999);
client.set('foo', 'bar', function (err, res) { client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, 'Stream connection ended and running command aborted. It might have been processed.'); assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
called = true; called = true;
}); });
var bool = client.quit(function (err, res) { var bool = client.quit(function (err, res) {
@@ -259,13 +259,12 @@ describe('connection tests', function () {
it('emits error once if reconnecting after command has been executed but not yet returned without callback', function (done) { it('emits error once if reconnecting after command has been executed but not yet returned without callback', function (done) {
client = redis.createClient.apply(null, args); client = redis.createClient.apply(null, args);
client.on('error', function (err) {
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
done();
});
client.on('ready', function () { client.on('ready', function () {
client.set('foo', 'bar'); client.set('foo', 'bar', function (err) {
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
done();
});
// Abort connection before the value returned // Abort connection before the value returned
client.stream.destroy(); client.stream.destroy();
}); });
@@ -281,7 +280,8 @@ describe('connection tests', function () {
retryStrategy: function (options) { retryStrategy: function (options) {
if (options.totalRetryTime > 150) { if (options.totalRetryTime > 150) {
client.set('foo', 'bar', function (err, res) { client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, 'Connection timeout'); assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
assert.strictEqual(err.origin.message, 'Connection timeout');
done(); done();
}); });
// Pass a individual error message to the error handler // Pass a individual error message to the error handler
@@ -308,7 +308,9 @@ describe('connection tests', function () {
retry_strategy: function (options) { retry_strategy: function (options) {
if (options.total_retry_time > 150) { if (options.total_retry_time > 150) {
client.set('foo', 'bar', function (err, res) { client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.code, 'ECONNREFUSED'); assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
assert.strictEqual(err.code, 'NR_CLOSED');
assert.strictEqual(err.origin.code, 'ECONNREFUSED');
done(); done();
}); });
return false; return false;
@@ -319,10 +321,15 @@ describe('connection tests', function () {
}); });
}); });
it('retry_strategy used to reconnect with defaults', function (done) { it('retryStrategy used to reconnect with defaults', function (done) {
var unhookIntercept = intercept(function () {
return '';
});
redis.debugMode = true;
client = redis.createClient({ client = redis.createClient({
retry_strategy: function (options) { retryStrategy: function (options) {
client.set('foo', 'bar'); client.set('foo', 'bar');
assert(redis.debugMode);
return null; return null;
} }
}); });
@@ -330,9 +337,10 @@ describe('connection tests', function () {
client.stream.destroy(); client.stream.destroy();
}, 50); }, 50);
client.on('error', function (err) { client.on('error', function (err) {
assert.strictEqual(err.code, 'NR_OFFLINE'); assert.strictEqual(err.code, 'NR_CLOSED');
assert.strictEqual(err.errors.length, 1); assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
assert.notStrictEqual(err.message, err.errors[0].message); unhookIntercept();
redis.debugMode = false;
done(); done();
}); });
}); });

View File

@@ -111,7 +111,7 @@ describe("The 'multi' method", function () {
it('reports an error', function (done) { it('reports an error', function (done) {
var multi = client.multi(); var multi = client.multi();
var notBuffering = multi.exec(function (err, res) { var notBuffering = multi.exec(function (err, res) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
done(); done();
}); });
assert.strictEqual(notBuffering, false); assert.strictEqual(notBuffering, false);
@@ -119,7 +119,7 @@ describe("The 'multi' method", function () {
it('reports an error if promisified', function () { it('reports an error if promisified', function () {
return client.multi().execAsync().catch(function (err) { return client.multi().execAsync().catch(function (err) {
assert(err.message.match(/The connection has already been closed/)); assert(err.message.match(/The connection is already closed/));
}); });
}); });
}); });

View File

@@ -3,6 +3,7 @@
var assert = require('assert'); var assert = require('assert');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var intercept = require('intercept-stdout');
var config = require('./lib/config'); var config = require('./lib/config');
var helper = require('./helper'); var helper = require('./helper');
var utils = require('../lib/utils'); var utils = require('../lib/utils');
@@ -58,6 +59,12 @@ describe('The node_redis client', function () {
assert.strictEqual(client2.options[elem], client.options[elem]); assert.strictEqual(client2.options[elem], client.options[elem]);
} }
} }
client2.on('error', function (err) {
assert.strictEqual(err.message, 'Connection forcefully ended and command aborted. It might have been processed.');
assert.strictEqual(err.command, 'SELECT');
assert(err instanceof Error);
assert.strictEqual(err.name, 'AbortError');
});
client2.on('ready', function () { client2.on('ready', function () {
client2.end(true); client2.end(true);
done(); done();
@@ -193,10 +200,10 @@ describe('The node_redis client', function () {
}); });
}); });
it('using multi with send_command should work as individual command instead of using the internal multi', function (done) { it('using multi with sendCommand should work as individual command instead of using the internal multi', function (done) {
// This is necessary to keep backwards compatibility and it is the only way to handle multis as you want in node_redis // This is necessary to keep backwards compatibility and it is the only way to handle multis as you want in node_redis
client.send_command('multi'); client.sendCommand('multi');
client.send_command('set', ['foo', 'bar'], helper.isString('QUEUED')); client.sendCommand('set', ['foo', 'bar'], helper.isString('QUEUED'));
client.get('foo'); client.get('foo');
client.exec(function (err, res) { // exec is not manipulated if not fired by the individual multi command client.exec(function (err, res) { // exec is not manipulated if not fired by the individual multi command
// As the multi command is handled individually by the user he also has to handle the return value // As the multi command is handled individually by the user he also has to handle the return value
@@ -340,7 +347,8 @@ describe('The node_redis client', function () {
} }
}, 20); }, 20);
var cb = function (err, res) { var cb = function (err, res) {
assert(/The connection has already been closed/.test(err.message)); assert(/Connection forcefully ended|The connection is already closed./.test(err.message));
assert.strictEqual(err.code, 'NR_CLOSED');
end(); end();
}; };
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
@@ -361,7 +369,7 @@ describe('The node_redis client', function () {
done(); done();
}, 20); }, 20);
var cb = function (err, res) { var cb = function (err, res) {
assert(/The connection has already been closed./.test(err.message)); assert(/Connection forcefully ended|The connection is already closed./.test(err.message));
end(); end();
}; };
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
@@ -373,6 +381,59 @@ describe('The node_redis client', function () {
} }
}); });
it('emits an aggregate error if no callback was present for multiple commands in debug_mode', function (done) {
redis.debug_mode = true;
var unhookIntercept = intercept(function (data) {
return ''; // Don't print the debug messages
});
client.set('foo', 'bar');
client.set('baz', 'hello world');
client.on('error', function (err) {
assert(err instanceof Error);
assert(err instanceof redis.AbortError);
assert(err instanceof redis.AggregateError);
assert.strictEqual(err.name, 'AggregateError');
assert.strictEqual(err.errors.length, 2);
assert.strictEqual(err.message, 'Connection forcefully ended and commands aborted.');
assert.strictEqual(err.code, 'NR_CLOSED');
assert.strictEqual(err.errors[0].message, 'Connection forcefully ended and command aborted. It might have been processed.');
assert.strictEqual(err.errors[0].command, 'SET');
assert.strictEqual(err.errors[0].code, 'NR_CLOSED');
assert.deepEqual(err.errors[0].args, ['foo', 'bar']);
done();
});
client.end(true);
unhookIntercept();
redis.debug_mode = false;
});
it('emits an abort error if no callback was present for a single commands', function (done) {
redis.debug_mode = true;
var unhookIntercept = intercept(function (data) {
return ''; // Don't print the debug messages
});
client.set('foo', 'bar');
client.on('error', function (err) {
assert(err instanceof Error);
assert(err instanceof redis.AbortError);
assert(!(err instanceof redis.AggregateError));
assert.strictEqual(err.message, 'Connection forcefully ended and command aborted. It might have been processed.');
assert.strictEqual(err.command, 'SET');
assert.strictEqual(err.code, 'NR_CLOSED');
assert.deepEqual(err.args, ['foo', 'bar']);
done();
});
client.end(true);
unhookIntercept();
redis.debug_mode = false;
});
it('does not emit abort errors if no callback was present while not being in debug_mode ', function (done) {
client.set('foo', 'bar');
client.end(true);
setTimeout(done, 100);
});
}); });
describe('commands after using .quit should fail', function () { describe('commands after using .quit should fail', function () {
@@ -385,7 +446,7 @@ describe('The node_redis client', function () {
client = redis.createClient(); client = redis.createClient();
client.quit(function () { client.quit(function () {
client.get('foo', function (err, res) { client.get('foo', function (err, res) {
assert.strictEqual(err.message, 'Stream connection ended and running command aborted. It might have been processed.'); assert.strictEqual(err.message, 'Stream connection ended and command aborted. It might have been processed.');
assert.strictEqual(client.offline_queue.length, 0); assert.strictEqual(client.offline_queue.length, 0);
done(); done();
}); });
@@ -398,7 +459,7 @@ describe('The node_redis client', function () {
client.quit(); client.quit();
setTimeout(function () { setTimeout(function () {
client.get('foo', function (err, res) { client.get('foo', function (err, res) {
assert.strictEqual(err.message, 'GET can\'t be processed. The connection has already been closed.'); assert.strictEqual(err.message, 'GET can\'t be processed. The connection is already closed.');
assert.strictEqual(err.command, 'GET'); assert.strictEqual(err.command, 'GET');
assert.strictEqual(client.offline_queue.length, 0); assert.strictEqual(client.offline_queue.length, 0);
done(); done();
@@ -410,7 +471,7 @@ describe('The node_redis client', function () {
if (helper.redisProcess().spawnFailed()) this.skip(); if (helper.redisProcess().spawnFailed()) this.skip();
client.quit(); client.quit();
client.on('error', function (err) { client.on('error', function (err) {
assert.strictEqual(err.message, 'SET can\'t be processed. The connection has already been closed.'); assert.strictEqual(err.message, 'SET can\'t be processed. The connection is already closed.');
assert.strictEqual(err.command, 'SET'); assert.strictEqual(err.command, 'SET');
assert.strictEqual(client.offline_queue_length, 0); assert.strictEqual(client.offline_queue_length, 0);
done(); done();
@@ -542,7 +603,7 @@ describe('The node_redis client', function () {
}); });
domain.on('error', function (err) { domain.on('error', function (err) {
assert.strictEqual(err.message, 'SET can\'t be processed. The connection has already been closed.'); assert.strictEqual(err.message, 'SET can\'t be processed. The connection is already closed.');
domain.exit(); domain.exit();
done(); done();
}); });
@@ -919,8 +980,11 @@ describe('The node_redis client', function () {
it('should gracefully recover and only fail on the already send commands', function (done) { it('should gracefully recover and only fail on the already send commands', function (done) {
client = redis.createClient.apply(null, args); client = redis.createClient.apply(null, args);
var error;
client.on('error', function (err) { client.on('error', function (err) {
assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte'); assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte. Please report this.');
assert.strictEqual(err, error);
assert(err instanceof redis.ReplyError);
// After the hard failure work properly again. The set should have been processed properly too // After the hard failure work properly again. The set should have been processed properly too
client.get('foo', function (err, res) { client.get('foo', function (err, res) {
assert.strictEqual(res, 'bar'); assert.strictEqual(res, 'bar');
@@ -929,7 +993,10 @@ describe('The node_redis client', function () {
}); });
client.once('ready', function () { client.once('ready', function () {
client.set('foo', 'bar', function (err, res) { client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte'); assert.strictEqual(err.message, 'Fatal error encountert. Command aborted. It might have been processed.');
assert.strictEqual(err.code, 'NR_FATAL');
assert(err instanceof redis.AbortError);
error = err.origin;
}); });
// Fail the set answer. Has no corresponding command obj and will therefore land in the error handler and set // Fail the set answer. Has no corresponding command obj and will therefore land in the error handler and set
client.reply_parser.execute(new Buffer('a*1\r*1\r$1`zasd\r\na')); client.reply_parser.execute(new Buffer('a*1\r*1\r$1`zasd\r\na'));
@@ -974,7 +1041,7 @@ describe('The node_redis client', function () {
setTimeout(function () { setTimeout(function () {
client.set('foo', 'bar', function (err, result) { client.set('foo', 'bar', function (err, result) {
if (!finished) done(err); if (!finished) done(err);
assert.strictEqual(err.message, "The command can't be processed. The connection has already been closed."); assert.strictEqual(err.message, 'Connection forcefully ended and command aborted.');
}); });
setTimeout(function () { setTimeout(function () {
@@ -993,10 +1060,15 @@ describe('The node_redis client', function () {
var i = 0; var i = 0;
client.on('error', function (err) { client.on('error', function (err) {
if (err.message === 'Redis connection in broken state: maximum connection attempts exceeded.') { if (err.code === 'CONNECTION_BROKEN') {
assert(i, 3); assert(i, 3);
assert.strictEqual(client.offline_queue.length, 0); assert.strictEqual(client.offline_queue.length, 0);
done(); assert.strictEqual(err.origin.code, 'ECONNREFUSED');
if (!(err instanceof redis.AbortError)) {
done();
} else {
assert.strictEqual(err.command, 'SET');
}
} else { } else {
assert.equal(err.code, 'ECONNREFUSED'); assert.equal(err.code, 'ECONNREFUSED');
assert.equal(err.errno, 'ECONNREFUSED'); assert.equal(err.errno, 'ECONNREFUSED');
@@ -1111,10 +1183,13 @@ describe('The node_redis client', function () {
it('flushes the command queue if connection is lost', function (done) { it('flushes the command queue if connection is lost', function (done) {
client = redis.createClient({ client = redis.createClient({
parser: parser, parser: parser,
max_attempts: 2,
enable_offline_queue: false enable_offline_queue: false
}); });
redis.debug_mode = true;
var unhookIntercept = intercept(function () {
return '';
});
client.once('ready', function () { client.once('ready', function () {
var multi = client.multi(); var multi = client.multi();
multi.config('bar'); multi.config('bar');
@@ -1133,18 +1208,20 @@ describe('The node_redis client', function () {
var end = helper.callFuncAfter(done, 3); var end = helper.callFuncAfter(done, 3);
client.on('error', function (err) { client.on('error', function (err) {
assert.equal(client.command_queue.length, 0);
if (err.command === 'EXEC') { if (err.command === 'EXEC') {
assert.equal(client.command_queue.length, 0);
assert.equal(err.errors.length, 9); assert.equal(err.errors.length, 9);
end(); end();
} else if (err.code === 'UNCERTAIN_STATE') { } else if (err.code === 'UNCERTAIN_STATE') {
assert.equal(client.command_queue.length, 0);
assert.equal(err.errors.length, 4); assert.equal(err.errors.length, 4);
end(); end();
} else { } else {
assert.equal(err.code, 'ECONNREFUSED'); assert.equal(err.code, 'ECONNREFUSED');
assert.equal(err.errno, 'ECONNREFUSED'); assert.equal(err.errno, 'ECONNREFUSED');
assert.equal(err.syscall, 'connect'); assert.equal(err.syscall, 'connect');
redis.debug_mode = false;
client.end(true);
unhookIntercept();
end(); end();
} }
}); });