You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-09 00:22:08 +03:00
Added individual error classes Don't silently fail for commands without callback from now on General polishing (e.g. better error messages) Fix typos
1235 lines
64 KiB
JavaScript
1235 lines
64 KiB
JavaScript
'use strict';
|
|
|
|
var assert = require('assert');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var intercept = require('intercept-stdout');
|
|
var config = require('./lib/config');
|
|
var helper = require('./helper');
|
|
var utils = require('../lib/utils');
|
|
var fork = require('child_process').fork;
|
|
var redis = config.redis;
|
|
|
|
describe('The node_redis client', function () {
|
|
|
|
it('individual commands sanity check', function (done) {
|
|
// All commands should work the same in multi context or without
|
|
// Therefor individual commands always have to be handled in both cases
|
|
fs.readFile(path.resolve(__dirname, '../lib/individualCommands.js'), 'utf8', function (err, data) {
|
|
var client_prototype = data.match(/(\n| = )RedisClient\.prototype.[a-zA-Z_]+/g);
|
|
var multi_prototype = data.match(/(\n| = )Multi\.prototype\.[a-zA-Z_]+/g);
|
|
// Check that every entry RedisClient entry has a correspondend Multi entry
|
|
assert.strictEqual(client_prototype.filter(function (entry) {
|
|
return multi_prototype.indexOf(entry.replace('RedisClient', 'Multi')) === -1;
|
|
}).length, 4); // multi and batch are included too
|
|
assert.strictEqual(client_prototype.length, multi_prototype.length + 4);
|
|
// Check that all entries exist in uppercase and in lowercase variants
|
|
assert.strictEqual(data.match(/(\n| = )RedisClient\.prototype.[a-z_]+/g).length * 2, client_prototype.length);
|
|
done();
|
|
});
|
|
});
|
|
|
|
helper.allTests(function (parser, ip, args) {
|
|
|
|
describe('using ' + parser + ' and ' + ip, function () {
|
|
var client;
|
|
|
|
afterEach(function () {
|
|
client.end(true);
|
|
});
|
|
|
|
describe('when connected', function () {
|
|
beforeEach(function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.once('connect', function () {
|
|
client.flushdb(done);
|
|
});
|
|
});
|
|
|
|
describe('duplicate', function () {
|
|
it('check if all options got copied properly', function (done) {
|
|
client.selected_db = 2;
|
|
var client2 = client.duplicate();
|
|
assert.strictEqual(client.connectionId + 1, client2.connection_id);
|
|
assert.strictEqual(client2.selected_db, 2);
|
|
assert(client.connected);
|
|
assert(!client2.connected);
|
|
for (var elem in client.options) {
|
|
if (client.options.hasOwnProperty(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.end(true);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('check if all new options replaced the old ones', function (done) {
|
|
var client2 = client.duplicate({
|
|
no_ready_check: true
|
|
});
|
|
assert(client.connected);
|
|
assert(!client2.connected);
|
|
assert.strictEqual(client.options.no_ready_check, undefined);
|
|
assert.strictEqual(client2.options.no_ready_check, true);
|
|
assert.notDeepEqual(client.options, client2.options);
|
|
for (var elem in client.options) {
|
|
if (client.options.hasOwnProperty(elem)) {
|
|
if (elem !== 'no_ready_check') {
|
|
assert.strictEqual(client2.options[elem], client.options[elem]);
|
|
}
|
|
}
|
|
}
|
|
client2.on('ready', function () {
|
|
client2.end(true);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('works with a callback', function (done) {
|
|
client.duplicate(function (err, client) {
|
|
assert(!err);
|
|
assert.strictEqual(client.ready, true);
|
|
client.quit(done);
|
|
});
|
|
});
|
|
|
|
it('works with a callback and errors out', function (done) {
|
|
client.duplicate({
|
|
port: '9999'
|
|
}, function (err, client) {
|
|
assert.strictEqual(err.code, 'ECONNREFUSED');
|
|
done(client);
|
|
});
|
|
});
|
|
|
|
it('works with a promises', function () {
|
|
return client.duplicateAsync().then(function (client) {
|
|
assert.strictEqual(client.ready, true);
|
|
return client.quitAsync();
|
|
});
|
|
});
|
|
|
|
it('works with a promises and errors', function () {
|
|
return client.duplicateAsync({
|
|
port: 9999
|
|
}).catch(function (err) {
|
|
assert.strictEqual(err.code, 'ECONNREFUSED');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('big data', function () {
|
|
|
|
// Check if the fast mode for big strings is working correct
|
|
it('safe strings that are bigger than 30000 characters', function (done) {
|
|
var str = 'foo ಠ_ಠ bar ';
|
|
while (str.length < 111111) {
|
|
str += str;
|
|
}
|
|
client.set('foo', str);
|
|
client.get('foo', function (err, res) {
|
|
assert.strictEqual(res, str);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('safe strings that are bigger than 30000 characters with multi', function (done) {
|
|
var str = 'foo ಠ_ಠ bar ';
|
|
while (str.length < 111111) {
|
|
str += str;
|
|
}
|
|
var called = false;
|
|
var temp = client.write_buffers.bind(client);
|
|
assert(client.fire_strings);
|
|
client.write_buffers = function (data) {
|
|
called = true;
|
|
// To increase write performance for strings the value is converted to a buffer
|
|
assert(!client.fire_strings);
|
|
temp(data);
|
|
};
|
|
client.multi().set('foo', str).get('foo', function (err, res) {
|
|
assert.strictEqual(res, str);
|
|
}).exec(function (err, res) {
|
|
assert(called);
|
|
assert.strictEqual(res[1], str);
|
|
done();
|
|
});
|
|
assert(client.fire_strings);
|
|
});
|
|
});
|
|
|
|
describe('send_command', function () {
|
|
|
|
it('omitting args should be fine', function (done) {
|
|
client.server_info = {};
|
|
client.send_command('info');
|
|
client.send_command('ping', function (err, res) {
|
|
assert.strictEqual(res, 'PONG');
|
|
// Check if the previous info command used the internal individual info command
|
|
assert.notDeepEqual(client.server_info, {});
|
|
client.server_info = {};
|
|
});
|
|
client.send_command('info', null, undefined);
|
|
client.send_command('ping', null, function (err, res) {
|
|
assert.strictEqual(res, 'PONG');
|
|
// Check if the previous info command used the internal individual info command
|
|
assert.notDeepEqual(client.server_info, {});
|
|
client.server_info = {};
|
|
});
|
|
client.send_command('info', undefined, undefined);
|
|
client.send_command('ping', function (err, res) {
|
|
assert.strictEqual(res, 'PONG');
|
|
// Check if the previous info command used the internal individual info command
|
|
assert.notDeepEqual(client.server_info, {});
|
|
client.server_info = {};
|
|
});
|
|
client.send_command('info', undefined, function (err, res) {
|
|
assert(/redis_version/.test(res));
|
|
// The individual info command should also be called by using send_command
|
|
// console.log(info, client.server_info);
|
|
assert.notDeepEqual(client.server_info, {});
|
|
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
|
|
client.sendCommand('multi');
|
|
client.sendCommand('set', ['foo', 'bar'], helper.isString('QUEUED'));
|
|
client.get('foo');
|
|
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
|
|
assert.strictEqual(res[0].toString(), 'OK');
|
|
assert.strictEqual(res[1].toString(), 'bar');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('multi should be handled special', function (done) {
|
|
client.send_command('multi', undefined, helper.isString('OK'));
|
|
var args = ['test', 'bla'];
|
|
client.send_command('set', args, helper.isString('QUEUED'));
|
|
assert.deepEqual(args, ['test', 'bla']); // Check args manipulation
|
|
client.get('test', helper.isString('QUEUED'));
|
|
client.exec(function (err, res) {
|
|
// As the multi command is handled individually by the user he also has to handle the return value
|
|
assert.strictEqual(res[0].toString(), 'OK');
|
|
assert.strictEqual(res[1].toString(), 'bla');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('using another type as cb should throw', function () {
|
|
try {
|
|
client.send_command('set', ['test', 'bla'], [true]);
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "Array" for callback function');
|
|
}
|
|
try {
|
|
client.send_command('set', ['test', 'bla'], null);
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "null" for callback function');
|
|
}
|
|
});
|
|
|
|
it('command argument has to be of type string', function () {
|
|
try {
|
|
client.send_command(true, ['test', 'bla'], function () {});
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "Boolean" for command name');
|
|
}
|
|
try {
|
|
client.send_command(undefined, ['test', 'bla'], function () {});
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "undefined" for command name');
|
|
}
|
|
try {
|
|
client.send_command(null, ['test', 'bla'], function () {});
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "null" for command name');
|
|
}
|
|
});
|
|
|
|
it('args may only be of type Array or undefined', function () {
|
|
try {
|
|
client.send_command('info', 123);
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "Number" for args');
|
|
}
|
|
});
|
|
|
|
it('passing a callback as args and as callback should throw', function () {
|
|
try {
|
|
client.send_command('info', function a () {}, function b () {});
|
|
throw new Error('failed');
|
|
} catch (err) {
|
|
assert.strictEqual(err.message, 'Wrong input type "Function" for args');
|
|
}
|
|
});
|
|
|
|
it('multi should be handled special', function (done) {
|
|
client.send_command('multi', undefined, helper.isString('OK'));
|
|
var args = ['test', 'bla'];
|
|
client.send_command('set', args, helper.isString('QUEUED'));
|
|
assert.deepEqual(args, ['test', 'bla']); // Check args manipulation
|
|
client.get('test', helper.isString('QUEUED'));
|
|
client.exec(function (err, res) {
|
|
// As the multi command is handled individually by the user he also has to handle the return value
|
|
assert.strictEqual(res[0].toString(), 'OK');
|
|
assert.strictEqual(res[1].toString(), 'bla');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('the args array may contain a arbitrary number of arguments', function (done) {
|
|
client.send_command('mset', ['foo', 1, 'bar', 2, 'baz', 3], helper.isString('OK'));
|
|
client.mget(['foo', 'bar', 'baz'], function (err, res) {
|
|
// As the multi command is handled individually by the user he also has to handle the return value
|
|
assert.strictEqual(res[0].toString(), '1');
|
|
assert.strictEqual(res[1].toString(), '2');
|
|
assert.strictEqual(res[2].toString(), '3');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('send_command with callback as args', function (done) {
|
|
client.send_command('abcdef', function (err, res) {
|
|
assert.strictEqual(err.message, "ERR unknown command 'abcdef'");
|
|
done();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe('retry_unfulfilled_commands', function () {
|
|
|
|
it('should retry all commands instead of returning an error if a command did not yet return after a connection loss', function (done) {
|
|
var bclient = redis.createClient({
|
|
parser: parser,
|
|
retry_unfulfilled_commands: true
|
|
});
|
|
bclient.blpop('blocking list 2', 5, function (err, value) {
|
|
assert.strictEqual(value[0], 'blocking list 2');
|
|
assert.strictEqual(value[1], 'initial value');
|
|
done(err);
|
|
});
|
|
bclient.once('ready', function () {
|
|
setTimeout(function () {
|
|
bclient.stream.destroy();
|
|
client.rpush('blocking list 2', 'initial value', helper.isNumber(1));
|
|
}, 100);
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe('.end', function () {
|
|
|
|
it('used without flush / flush set to false', function (done) {
|
|
var finished = false;
|
|
var end = helper.callFuncAfter(function () {
|
|
if (!finished) {
|
|
done(new Error('failed'));
|
|
}
|
|
}, 20);
|
|
var cb = function (err, res) {
|
|
assert(/Connection forcefully ended|The connection is already closed./.test(err.message));
|
|
assert.strictEqual(err.code, 'NR_CLOSED');
|
|
end();
|
|
};
|
|
for (var i = 0; i < 20; i++) {
|
|
if (i === 10) {
|
|
client.end();
|
|
}
|
|
client.set('foo', 'bar', cb);
|
|
}
|
|
client.on('warning', function () {}); // Ignore deprecation message
|
|
setTimeout(function () {
|
|
finished = true;
|
|
done();
|
|
}, 25);
|
|
});
|
|
|
|
it('used with flush set to true', function (done) {
|
|
var end = helper.callFuncAfter(function () {
|
|
done();
|
|
}, 20);
|
|
var cb = function (err, res) {
|
|
assert(/Connection forcefully ended|The connection is already closed./.test(err.message));
|
|
end();
|
|
};
|
|
for (var i = 0; i < 20; i++) {
|
|
if (i === 10) {
|
|
client.end(true);
|
|
client.stream.write('foo'); // Trigger an error on the closed stream that we ignore
|
|
}
|
|
client.set('foo', 'bar', cb);
|
|
}
|
|
});
|
|
|
|
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 () {
|
|
|
|
it('return an error in the callback', function (done) {
|
|
if (helper.redisProcess().spawnFailed()) this.skip();
|
|
|
|
// TODO: Investigate why this test is failing hard and killing mocha if using '/tmp/redis.sock'.
|
|
// Seems like something is wrong with nyc while passing a socket connection to create client!
|
|
client = redis.createClient();
|
|
client.quit(function () {
|
|
client.get('foo', function (err, res) {
|
|
assert.strictEqual(err.message, 'Stream connection ended and command aborted. It might have been processed.');
|
|
assert.strictEqual(client.offline_queue.length, 0);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('return an error in the callback version two', function (done) {
|
|
if (helper.redisProcess().spawnFailed()) this.skip();
|
|
|
|
client.quit();
|
|
setTimeout(function () {
|
|
client.get('foo', function (err, res) {
|
|
assert.strictEqual(err.message, 'GET can\'t be processed. The connection is already closed.');
|
|
assert.strictEqual(err.command, 'GET');
|
|
assert.strictEqual(client.offline_queue.length, 0);
|
|
done();
|
|
});
|
|
}, 50);
|
|
});
|
|
|
|
it('emit an error', function (done) {
|
|
if (helper.redisProcess().spawnFailed()) this.skip();
|
|
client.quit();
|
|
client.on('error', function (err) {
|
|
assert.strictEqual(err.message, 'SET can\'t be processed. The connection is already closed.');
|
|
assert.strictEqual(err.command, 'SET');
|
|
assert.strictEqual(client.offline_queue_length, 0);
|
|
done();
|
|
});
|
|
setTimeout(function () {
|
|
client.set('foo', 'bar');
|
|
}, 50);
|
|
});
|
|
|
|
});
|
|
|
|
describe('when redis closes unexpectedly', function () {
|
|
it('reconnects and can retrieve the pre-existing data', function (done) {
|
|
client.on('reconnecting', function on_recon (params) {
|
|
client.on('connect', function on_connect () {
|
|
var end = helper.callFuncAfter(function () {
|
|
client.removeListener('connect', on_connect);
|
|
client.removeListener('reconnecting', on_recon);
|
|
assert.strictEqual(client.server_info.db0.keys, 2);
|
|
assert.strictEqual(Object.keys(client.server_info.db0).length, 3);
|
|
done();
|
|
}, 4);
|
|
client.get('recon 1', helper.isString('one', end));
|
|
client.get('recon 1', helper.isString('one', end));
|
|
client.get('recon 2', helper.isString('two', end));
|
|
client.get('recon 2', helper.isString('two', end));
|
|
});
|
|
});
|
|
|
|
client.set('recon 1', 'one');
|
|
client.set('recon 2', 'two', function (err, res) {
|
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
|
// For orderly shutdown in normal programs, do client.quit()
|
|
client.stream.destroy();
|
|
});
|
|
});
|
|
|
|
it('reconnects properly when monitoring', function (done) {
|
|
client.on('reconnecting', function on_recon (params) {
|
|
client.on('ready', function on_ready () {
|
|
assert.strictEqual(client.monitoring, true, 'monitoring after reconnect');
|
|
client.removeListener('ready', on_ready);
|
|
client.removeListener('reconnecting', on_recon);
|
|
done();
|
|
});
|
|
});
|
|
|
|
assert.strictEqual(client.monitoring, false, 'monitoring off at start');
|
|
client.set('recon 1', 'one');
|
|
client.monitor(function (err, res) {
|
|
assert.strictEqual(client.monitoring, true, 'monitoring on after monitor()');
|
|
client.set('recon 2', 'two', function (err, res) {
|
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
|
// For orderly shutdown in normal programs, do client.quit()
|
|
client.stream.destroy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("and it's subscribed to a channel", function () {
|
|
// "Connection in subscriber mode, only subscriber commands may be used"
|
|
it('reconnects, unsubscribes, and can retrieve the pre-existing data', function (done) {
|
|
client.on('ready', function on_connect () {
|
|
client.unsubscribe(helper.isNotError());
|
|
|
|
client.on('unsubscribe', function (channel, count) {
|
|
// we should now be out of subscriber mode.
|
|
assert.strictEqual(channel, 'recon channel');
|
|
assert.strictEqual(count, 0);
|
|
client.set('foo', 'bar', helper.isString('OK', done));
|
|
});
|
|
});
|
|
|
|
client.set('recon 1', 'one');
|
|
client.subscribe('recon channel', function (err, res) {
|
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
|
// For orderly shutdown in normal programs, do client.quit()
|
|
client.stream.destroy();
|
|
});
|
|
});
|
|
|
|
it('reconnects, unsubscribes, and can retrieve the pre-existing data of a explicit channel', function (done) {
|
|
client.on('ready', function on_connect () {
|
|
client.unsubscribe('recon channel', helper.isNotError());
|
|
|
|
client.on('unsubscribe', function (channel, count) {
|
|
// we should now be out of subscriber mode.
|
|
assert.strictEqual(channel, 'recon channel');
|
|
assert.strictEqual(count, 0);
|
|
client.set('foo', 'bar', helper.isString('OK', done));
|
|
});
|
|
});
|
|
|
|
client.set('recon 1', 'one');
|
|
client.subscribe('recon channel', function (err, res) {
|
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
|
// For orderly shutdown in normal programs, do client.quit()
|
|
client.stream.destroy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('domain', function () {
|
|
it('allows client to be executed from within domain', function (done) {
|
|
var domain = require('domain').create();
|
|
|
|
domain.run(function () {
|
|
client.set('domain', 'value', function (err, res) {
|
|
assert.ok(process.domain);
|
|
throw new Error('ohhhh noooo');
|
|
});
|
|
});
|
|
|
|
// this is the expected and desired behavior
|
|
domain.on('error', function (err) {
|
|
assert.strictEqual(err.message, 'ohhhh noooo');
|
|
domain.exit();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('catches all errors from within the domain', function (done) {
|
|
var domain = require('domain').create();
|
|
|
|
domain.run(function () {
|
|
// Trigger an error within the domain
|
|
client.end(true);
|
|
client.set('domain', 'value');
|
|
});
|
|
|
|
domain.on('error', function (err) {
|
|
assert.strictEqual(err.message, 'SET can\'t be processed. The connection is already closed.');
|
|
domain.exit();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('monitor', function () {
|
|
it('monitors commands on all redis clients and works in the correct order', function (done) {
|
|
var monitorClient = redis.createClient.apply(null, args);
|
|
var responses = [];
|
|
var end = helper.callFuncAfter(done, 5);
|
|
|
|
monitorClient.set('foo', 'bar');
|
|
monitorClient.flushdb();
|
|
monitorClient.monitor(function (err, res) {
|
|
assert.strictEqual(res, 'OK');
|
|
client.mget('some', 'keys', 'foo', 'bar');
|
|
client.set('json', JSON.stringify({
|
|
foo: '123',
|
|
bar: 'sdflkdfsjk',
|
|
another: false
|
|
}));
|
|
monitorClient.get('baz', function (err, res) {
|
|
assert.strictEqual(res, null);
|
|
end(err);
|
|
});
|
|
monitorClient.set('foo', 'bar" "s are " " good!"', function (err, res) {
|
|
assert.strictEqual(res, 'OK');
|
|
end(err);
|
|
});
|
|
monitorClient.mget('foo', 'baz', function (err, res) {
|
|
assert.strictEqual(res[0], 'bar" "s are " " good!"');
|
|
assert.strictEqual(res[1], null);
|
|
end(err);
|
|
});
|
|
monitorClient.subscribe('foo', 'baz', function (err, res) {
|
|
// The return value might change in v.3
|
|
// assert.strictEqual(res, 'baz');
|
|
// TODO: Fix the return value of subscribe calls
|
|
end(err);
|
|
});
|
|
});
|
|
|
|
monitorClient.on('monitor', function (time, args, rawOutput) {
|
|
responses.push(args);
|
|
assert(utils.monitor_regex.test(rawOutput), rawOutput);
|
|
if (responses.length === 6) {
|
|
assert.deepEqual(responses[0], ['mget', 'some', 'keys', 'foo', 'bar']);
|
|
assert.deepEqual(responses[1], ['set', 'json', '{"foo":"123","bar":"sdflkdfsjk","another":false}']);
|
|
assert.deepEqual(responses[2], ['get', 'baz']);
|
|
assert.deepEqual(responses[3], ['set', 'foo', 'bar" "s are " " good!"']);
|
|
assert.deepEqual(responses[4], ['mget', 'foo', 'baz']);
|
|
assert.deepEqual(responses[5], ['subscribe', 'foo', 'baz']);
|
|
monitorClient.quit(end);
|
|
}
|
|
});
|
|
});
|
|
|
|
it('monitors returns strings in the rawOutput even with return_buffers activated', function (done) {
|
|
var monitorClient = redis.createClient({
|
|
return_buffers: true
|
|
});
|
|
|
|
monitorClient.MONITOR(function (err, res) {
|
|
assert.strictEqual(res.inspect(), new Buffer('OK').inspect());
|
|
client.mget('hello', new Buffer('world'));
|
|
});
|
|
|
|
monitorClient.on('monitor', function (time, args, rawOutput) {
|
|
assert.strictEqual(typeof rawOutput, 'string');
|
|
assert(utils.monitor_regex.test(rawOutput), rawOutput);
|
|
assert.deepEqual(args, ['mget', 'hello', 'world']);
|
|
// Quit immediatly ends monitoring mode and therefore does not stream back the quit command
|
|
monitorClient.quit(done);
|
|
});
|
|
});
|
|
|
|
it('monitors reconnects properly and works with the offline queue', function (done) {
|
|
var i = 0;
|
|
client.MONITOR(helper.isString('OK'));
|
|
client.mget('hello', 'world');
|
|
client.on('monitor', function (time, args, rawOutput) {
|
|
assert(utils.monitor_regex.test(rawOutput), rawOutput);
|
|
assert.deepEqual(args, ['mget', 'hello', 'world']);
|
|
if (i++ === 2) {
|
|
// End after two reconnects
|
|
return done();
|
|
}
|
|
client.stream.destroy();
|
|
client.mget('hello', 'world');
|
|
});
|
|
});
|
|
|
|
it('monitors reconnects properly and works with the offline queue in a batch statement', function (done) {
|
|
var i = 0;
|
|
var multi = client.batch();
|
|
multi.MONITOR(helper.isString('OK'));
|
|
multi.mget('hello', 'world');
|
|
multi.exec(function (err, res) {
|
|
assert.deepEqual(res, ['OK', [null, null]]);
|
|
});
|
|
client.on('monitor', function (time, args, rawOutput) {
|
|
assert(utils.monitor_regex.test(rawOutput), rawOutput);
|
|
assert.deepEqual(args, ['mget', 'hello', 'world']);
|
|
if (i++ === 2) {
|
|
// End after two reconnects
|
|
return done();
|
|
}
|
|
client.stream.destroy();
|
|
client.mget('hello', 'world');
|
|
});
|
|
});
|
|
|
|
it('monitor does not activate if the command could not be processed properly', function (done) {
|
|
client.MONITOR(function (err, res) {
|
|
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
|
|
});
|
|
client.on('error', function (err) {}); // Ignore error here
|
|
client.stream.destroy();
|
|
client.on('monitor', function (time, args, rawOutput) {
|
|
done(new Error('failed')); // Should not be activated
|
|
});
|
|
client.on('reconnecting', function () {
|
|
client.get('foo', function (err, res) {
|
|
assert(!err);
|
|
assert.strictEqual(client.monitoring, false);
|
|
setTimeout(done, 10); // The monitor command might be returned a tiny bit later
|
|
});
|
|
});
|
|
});
|
|
|
|
it('monitors works in combination with the pub sub mode and the offline queue', function (done) {
|
|
var responses = [];
|
|
var pub = redis.createClient();
|
|
pub.on('ready', function () {
|
|
client.MONITOR(function (err, res) {
|
|
assert.strictEqual(res, 'OK');
|
|
pub.get('foo', helper.isNull());
|
|
});
|
|
client.subscribe('/foo', '/bar');
|
|
client.unsubscribe('/bar');
|
|
setTimeout(function () {
|
|
client.stream.destroy();
|
|
client.once('ready', function () {
|
|
pub.publish('/foo', 'hello world');
|
|
});
|
|
client.set('foo', 'bar', helper.isError());
|
|
client.subscribe('baz');
|
|
client.unsubscribe('baz');
|
|
}, 150);
|
|
var called = false;
|
|
client.on('monitor', function (time, args, rawOutput) {
|
|
responses.push(args);
|
|
assert(utils.monitor_regex.test(rawOutput), rawOutput);
|
|
if (responses.length === 7) {
|
|
assert.deepEqual(responses[0], ['subscribe', '/foo', '/bar']);
|
|
assert.deepEqual(responses[1], ['unsubscribe', '/bar']);
|
|
assert.deepEqual(responses[2], ['get', 'foo']);
|
|
assert.deepEqual(responses[3], ['subscribe', '/foo']);
|
|
assert.deepEqual(responses[4], ['subscribe', 'baz']);
|
|
assert.deepEqual(responses[5], ['unsubscribe', 'baz']);
|
|
assert.deepEqual(responses[6], ['publish', '/foo', 'hello world']);
|
|
// The publish is called right after the reconnect and the monitor is called before the message is emitted.
|
|
// Therefore we have to wait till the next tick
|
|
process.nextTick(function () {
|
|
assert(called);
|
|
client.quit(done);
|
|
pub.end(false);
|
|
});
|
|
}
|
|
});
|
|
client.on('message', function (channel, msg) {
|
|
assert.strictEqual(channel, '/foo');
|
|
assert.strictEqual(msg, 'hello world');
|
|
called = true;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('idle', function () {
|
|
it('emits idle as soon as there are no outstanding commands', function (done) {
|
|
var end = helper.callFuncAfter(done, 2);
|
|
client.on('warning', function (msg) {
|
|
assert.strictEqual(
|
|
msg,
|
|
'The idle event listener is deprecated and will likely be removed in v.3.0.0.\n' +
|
|
'If you rely on this feature please open a new ticket in node_redis with your use case'
|
|
);
|
|
end();
|
|
});
|
|
client.on('idle', function onIdle () {
|
|
client.removeListener('idle', onIdle);
|
|
client.get('foo', helper.isString('bar', end));
|
|
});
|
|
client.set('foo', 'bar');
|
|
});
|
|
});
|
|
|
|
describe('utf8', function () {
|
|
it('handles utf-8 keys', function (done) {
|
|
var utf8_sample = 'ಠ_ಠ';
|
|
client.set(['utf8test', utf8_sample], helper.isString('OK'));
|
|
client.get(['utf8test'], function (err, obj) {
|
|
assert.strictEqual(utf8_sample, obj);
|
|
done(err);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('unref', function () {
|
|
it('exits subprocess as soon as final command is processed', function (done) {
|
|
this.timeout(12000);
|
|
var args = config.HOST[ip] ? [config.HOST[ip], config.PORT] : [ip];
|
|
var external = fork('./test/lib/unref.js', args);
|
|
|
|
var id = setTimeout(function () {
|
|
external.kill();
|
|
done(Error('unref subprocess timed out'));
|
|
}, 8000);
|
|
|
|
external.on('close', function (code) {
|
|
clearTimeout(id);
|
|
assert.strictEqual(code, 0);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('execution order / fire query while loading', function () {
|
|
it('keep execution order for commands that may fire while redis is still loading', function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
var fired = false;
|
|
client.set('foo', 'bar', function (err, res) {
|
|
assert(fired === false);
|
|
done();
|
|
});
|
|
client.info(function (err, res) {
|
|
fired = true;
|
|
});
|
|
});
|
|
|
|
// TODO: consider allowing loading commands in v.3
|
|
// it('should fire early', function (done) {
|
|
// client = redis.createClient.apply(null, args);
|
|
// var fired = false;
|
|
// client.info(function (err, res) {
|
|
// fired = true;
|
|
// });
|
|
// client.set('foo', 'bar', function (err, res) {
|
|
// assert(fired);
|
|
// done();
|
|
// });
|
|
// assert.strictEqual(client.offline_queue.length, 1);
|
|
// assert.strictEqual(client.command_queue.length, 1);
|
|
// client.on('connect', function () {
|
|
// assert.strictEqual(client.offline_queue.length, 1);
|
|
// assert.strictEqual(client.command_queue.length, 1);
|
|
// });
|
|
// client.on('ready', function () {
|
|
// assert.strictEqual(client.offline_queue.length, 0);
|
|
// });
|
|
// });
|
|
});
|
|
|
|
describe('socket_nodelay', function () {
|
|
describe('true', function () {
|
|
var args = config.configureClient(parser, ip, {
|
|
socket_nodelay: true
|
|
});
|
|
|
|
it("fires client.on('ready')", function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.on('ready', function () {
|
|
assert.strictEqual(true, client.options.socket_nodelay);
|
|
client.quit(done);
|
|
});
|
|
});
|
|
|
|
it('client is functional', function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.on('ready', function () {
|
|
assert.strictEqual(true, client.options.socket_nodelay);
|
|
client.set(['set key 1', 'set val'], helper.isString('OK'));
|
|
client.set(['set key 2', 'set val'], helper.isString('OK'));
|
|
client.get(['set key 1'], helper.isString('set val'));
|
|
client.get(['set key 2'], helper.isString('set val'));
|
|
client.quit(done);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('false', function () {
|
|
var args = config.configureClient(parser, ip, {
|
|
socket_nodelay: false
|
|
});
|
|
|
|
it("fires client.on('ready')", function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.on('ready', function () {
|
|
assert.strictEqual(false, client.options.socket_nodelay);
|
|
client.quit(done);
|
|
});
|
|
});
|
|
|
|
it('client is functional', function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.on('ready', function () {
|
|
assert.strictEqual(false, client.options.socket_nodelay);
|
|
client.set(['set key 1', 'set val'], helper.isString('OK'));
|
|
client.set(['set key 2', 'set val'], helper.isString('OK'));
|
|
client.get(['set key 1'], helper.isString('set val'));
|
|
client.get(['set key 2'], helper.isString('set val'));
|
|
client.quit(done);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('defaults to true', function () {
|
|
|
|
it("fires client.on('ready')", function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.on('ready', function () {
|
|
assert.strictEqual(true, client.options.socket_nodelay);
|
|
client.quit(done);
|
|
});
|
|
});
|
|
|
|
it('client is functional', function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
client.on('ready', function () {
|
|
assert.strictEqual(true, client.options.socket_nodelay);
|
|
client.set(['set key 1', 'set val'], helper.isString('OK'));
|
|
client.set(['set key 2', 'set val'], helper.isString('OK'));
|
|
client.get(['set key 1'], helper.isString('set val'));
|
|
client.get(['set key 2'], helper.isString('set val'));
|
|
client.quit(done);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('retry_max_delay', function () {
|
|
it('sets upper bound on how long client waits before reconnecting', function (done) {
|
|
var time;
|
|
var timeout = process.platform !== 'win32' ? 20 : 100;
|
|
|
|
client = redis.createClient.apply(null, config.configureClient(parser, ip, {
|
|
retry_max_delay: 1 // ms
|
|
}));
|
|
client.on('ready', function () {
|
|
if (this.times_connected === 1) {
|
|
this.stream.end();
|
|
time = Date.now();
|
|
} else {
|
|
done();
|
|
}
|
|
});
|
|
client.on('reconnecting', function () {
|
|
time = Date.now() - time;
|
|
assert(time < timeout, 'The reconnect should not have taken longer than ' + timeout + ' but it took ' + time);
|
|
});
|
|
client.on('error', function (err) {
|
|
// This is rare but it might be triggered.
|
|
// So let's have a robust test
|
|
assert.strictEqual(err.code, 'ECONNRESET');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('protocol error', function () {
|
|
|
|
it('should gracefully recover and only fail on the already send commands', function (done) {
|
|
client = redis.createClient.apply(null, args);
|
|
var error;
|
|
client.on('error', function (err) {
|
|
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
|
|
client.get('foo', function (err, res) {
|
|
assert.strictEqual(res, 'bar');
|
|
done();
|
|
});
|
|
});
|
|
client.once('ready', function () {
|
|
client.set('foo', 'bar', function (err, res) {
|
|
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
|
|
client.reply_parser.execute(new Buffer('a*1\r*1\r$1`zasd\r\na'));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('enable_offline_queue', function () {
|
|
describe('true', function () {
|
|
it('should emit drain if offline queue is flushed and nothing to buffer', function (done) {
|
|
client = redis.createClient({
|
|
parser: parser,
|
|
no_ready_check: true
|
|
});
|
|
var end = helper.callFuncAfter(done, 3);
|
|
client.set('foo', 'bar');
|
|
client.get('foo', end);
|
|
client.on('warning', function (msg) {
|
|
assert.strictEqual(
|
|
msg,
|
|
'The drain event listener is deprecated and will be removed in v.3.0.0.\n' +
|
|
'If you want to keep on listening to this event please listen to the stream drain event directly.'
|
|
);
|
|
end();
|
|
});
|
|
client.on('drain', function () {
|
|
assert(client.offline_queue.length === 0);
|
|
end();
|
|
});
|
|
});
|
|
|
|
it('does not return an error and enqueues operation', function (done) {
|
|
client = redis.createClient(9999, null, {
|
|
max_attempts: 0,
|
|
parser: parser
|
|
});
|
|
var finished = false;
|
|
client.on('error', function (e) {
|
|
// ignore, b/c expecting a "can't connect" error
|
|
});
|
|
|
|
setTimeout(function () {
|
|
client.set('foo', 'bar', function (err, result) {
|
|
if (!finished) done(err);
|
|
assert.strictEqual(err.message, 'Connection forcefully ended and command aborted.');
|
|
});
|
|
|
|
setTimeout(function () {
|
|
assert.strictEqual(client.offline_queue.length, 1);
|
|
finished = true;
|
|
done();
|
|
}, 25);
|
|
}, 50);
|
|
});
|
|
|
|
it('enqueues operation and keep the queue while trying to reconnect', function (done) {
|
|
client = redis.createClient(9999, null, {
|
|
max_attempts: 4,
|
|
parser: parser
|
|
});
|
|
var i = 0;
|
|
|
|
client.on('error', function (err) {
|
|
if (err.code === 'CONNECTION_BROKEN') {
|
|
assert(i, 3);
|
|
assert.strictEqual(client.offline_queue.length, 0);
|
|
assert.strictEqual(err.origin.code, 'ECONNREFUSED');
|
|
if (!(err instanceof redis.AbortError)) {
|
|
done();
|
|
} else {
|
|
assert.strictEqual(err.command, 'SET');
|
|
}
|
|
} else {
|
|
assert.equal(err.code, 'ECONNREFUSED');
|
|
assert.equal(err.errno, 'ECONNREFUSED');
|
|
assert.equal(err.syscall, 'connect');
|
|
}
|
|
});
|
|
|
|
client.on('reconnecting', function (params) {
|
|
i++;
|
|
assert.equal(params.attempt, i);
|
|
assert.strictEqual(params.times_connected, 0);
|
|
assert(params.error instanceof Error);
|
|
assert(typeof params.total_retry_time === 'number');
|
|
assert.strictEqual(client.offline_queue.length, 2);
|
|
});
|
|
|
|
// Should work with either a callback or without
|
|
client.set('baz', 13);
|
|
client.set('foo', 'bar', function (err, result) {
|
|
assert(i, 3);
|
|
assert(err);
|
|
assert.strictEqual(client.offline_queue.length, 0);
|
|
});
|
|
});
|
|
|
|
it('flushes the command queue if connection is lost', function (done) {
|
|
client = redis.createClient({
|
|
parser: parser
|
|
});
|
|
|
|
client.once('ready', function () {
|
|
var multi = client.multi();
|
|
multi.config('bar');
|
|
var cb = function (err, reply) {
|
|
assert.equal(err.code, 'UNCERTAIN_STATE');
|
|
};
|
|
for (var i = 0; i < 12; i += 3) {
|
|
client.set('foo' + i, 'bar' + i);
|
|
multi.set('foo' + (i + 1), 'bar' + (i + 1), cb);
|
|
multi.set('foo' + (i + 2), 'bar' + (i + 2));
|
|
}
|
|
multi.exec();
|
|
assert.equal(client.command_queue_length, 15);
|
|
helper.killConnection(client);
|
|
});
|
|
|
|
var end = helper.callFuncAfter(done, 3);
|
|
client.on('error', function (err) {
|
|
if (err.command === 'EXEC') {
|
|
assert.strictEqual(client.command_queue.length, 0);
|
|
assert.strictEqual(err.errors.length, 9);
|
|
assert.strictEqual(err.errors[1].command, 'SET');
|
|
assert.deepEqual(err.errors[1].args, ['foo1', 'bar1']);
|
|
end();
|
|
} else if (err.code === 'UNCERTAIN_STATE') {
|
|
assert.strictEqual(client.command_queue.length, 0);
|
|
assert.strictEqual(err.errors.length, 4);
|
|
assert.strictEqual(err.errors[0].command, 'SET');
|
|
assert.deepEqual(err.errors[0].args, ['foo0', 'bar0']);
|
|
end();
|
|
} else {
|
|
assert.equal(err.code, 'ECONNREFUSED');
|
|
assert.equal(err.errno, 'ECONNREFUSED');
|
|
assert.equal(err.syscall, 'connect');
|
|
end();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('false', function () {
|
|
|
|
it('stream not writable', function (done) {
|
|
client = redis.createClient({
|
|
parser: parser,
|
|
enable_offline_queue: false
|
|
});
|
|
client.on('ready', function () {
|
|
client.stream.destroy();
|
|
client.set('foo', 'bar', function (err, res) {
|
|
assert.strictEqual(err.message, "SET can't be processed. Stream not writeable.");
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('emit an error and does not enqueues operation', function (done) {
|
|
client = redis.createClient(9999, null, {
|
|
parser: parser,
|
|
max_attempts: 0,
|
|
enable_offline_queue: false
|
|
});
|
|
var end = helper.callFuncAfter(done, 3);
|
|
|
|
client.on('error', function (err) {
|
|
assert(/offline queue is deactivated|ECONNREFUSED/.test(err.message));
|
|
assert.equal(client.command_queue.length, 0);
|
|
end();
|
|
});
|
|
|
|
client.set('foo', 'bar');
|
|
|
|
assert.doesNotThrow(function () {
|
|
client.set('foo', 'bar', function (err) {
|
|
// should callback with an error
|
|
assert.ok(err);
|
|
setTimeout(end, 50);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('flushes the command queue if connection is lost', function (done) {
|
|
client = redis.createClient({
|
|
parser: parser,
|
|
enable_offline_queue: false
|
|
});
|
|
|
|
redis.debug_mode = true;
|
|
var unhookIntercept = intercept(function () {
|
|
return '';
|
|
});
|
|
client.once('ready', function () {
|
|
var multi = client.multi();
|
|
multi.config('bar');
|
|
var cb = function (err, reply) {
|
|
assert.equal(err.code, 'UNCERTAIN_STATE');
|
|
};
|
|
for (var i = 0; i < 12; i += 3) {
|
|
client.set('foo' + i, 'bar' + i);
|
|
multi.set('foo' + (i + 1), 'bar' + (i + 1), cb);
|
|
multi.set('foo' + (i + 2), 'bar' + (i + 2));
|
|
}
|
|
multi.exec();
|
|
assert.equal(client.command_queue.length, 15);
|
|
helper.killConnection(client);
|
|
});
|
|
|
|
var end = helper.callFuncAfter(done, 3);
|
|
client.on('error', function (err) {
|
|
assert.equal(client.command_queue.length, 0);
|
|
if (err.command === 'EXEC') {
|
|
assert.equal(err.errors.length, 9);
|
|
end();
|
|
} else if (err.code === 'UNCERTAIN_STATE') {
|
|
assert.equal(err.errors.length, 4);
|
|
end();
|
|
} else {
|
|
assert.equal(err.code, 'ECONNREFUSED');
|
|
assert.equal(err.errno, 'ECONNREFUSED');
|
|
assert.equal(err.syscall, 'connect');
|
|
redis.debug_mode = false;
|
|
client.end(true);
|
|
unhookIntercept();
|
|
end();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
});
|
|
});
|