Browse Source

Merge pull request #998 from NodeRedis/2.5

2.5 pre-release

Fixes #479
Fixes #905
Fixes #958
internal
Ruben Bridgewater 9 years ago
parent
commit
5d1e9fe9e0
  1. 13
      .npmignore
  2. 63
      README.md
  3. 91
      benchmarks/buffer_bench.js
  4. 58
      benchmarks/multi_bench.js
  5. 33
      changelog.md
  6. 929
      index.js
  7. 4
      lib/command.js
  8. 86
      lib/commands.js
  9. 79
      lib/createClient.js
  10. 11
      lib/debug.js
  11. 137
      lib/individualCommands.js
  12. 224
      lib/multi.js
  13. 89
      lib/utils.js
  14. 3
      package.json
  15. 10
      test/auth.spec.js
  16. 23
      test/batch.spec.js
  17. 8
      test/commands/blpop.spec.js
  18. 8
      test/commands/get.spec.js
  19. 1
      test/commands/getset.spec.js
  20. 4
      test/commands/hgetall.spec.js
  21. 2
      test/commands/mset.spec.js
  22. 11
      test/commands/set.spec.js
  23. 99
      test/conect.slave.spec.js
  24. 19
      test/conf/faulty.cert
  25. 6
      test/conf/slave.conf
  26. 161
      test/connection.spec.js
  27. 41
      test/helper.js
  28. 45
      test/lib/redis-process.js
  29. 7
      test/lib/stunnel-process.js
  30. 35
      test/multi.spec.js
  31. 91
      test/node_redis.spec.js
  32. 29
      test/rename.spec.js
  33. 9
      test/return_buffers.spec.js
  34. 174
      test/tls.spec.js
  35. 241
      test/unify_options.spec.js
  36. 154
      test/utils.spec.js

13
.npmignore

@ -1,9 +1,10 @@
examples/
benches/
benchmarks/
test/
diff_multi_bench_output.js
generate_commands.js
multi_bench.js
test-unref.js
changelog.md
.nyc_output/
coverage/
.tern-port
*.log
*.rdb
*.out
*.yml

63
README.md

@ -118,7 +118,7 @@ Please be aware that sending null, undefined and Boolean values will result in t
# API
## Connection Events
## Connection and other Events
`client` will emit some events about the state of the connection to the Redis server.
@ -147,7 +147,7 @@ So please attach the error listener to node_redis.
`client` will emit `end` when an established Redis server connection has closed.
### "drain"
### "drain" (deprecated)
`client` will emit `drain` when the TCP connection to the Redis server has been buffering, but is now
writable. This event can be used to stream commands in to Redis and adapt to backpressure.
@ -155,10 +155,14 @@ writable. This event can be used to stream commands in to Redis and adapt to bac
If the stream is buffering `client.should_buffer` is set to true. Otherwise the variable is always set to false.
That way you can decide when to reduce your send rate and resume sending commands when you get `drain`.
You can also check the return value of each command as it will also return the backpressure indicator.
You can also check the return value of each command as it will also return the backpressure indicator (deprecated).
If false is returned the stream had to buffer.
### "idle"
### "warning"
`client` will emit `warning` when password was set but none is needed and if a deprecated option / function / similar is used.
### "idle" (deprecated)
`client` will emit `idle` when there are no outstanding commands that are awaiting a response.
@ -166,12 +170,13 @@ If false is returned the stream had to buffer.
If you have `redis-server` running on the same computer as node, then the defaults for
port and host are probably fine and you don't need to supply any arguments. `createClient()` returns a `RedisClient` object.
If the redis server runs on the same machine as the client consider using unix sockets if possible to increase throughput.
### overloading
* `redis.createClient()`
* `redis.createClient(options)`
* `redis.createClient(unix_socket, options)`
* `redis.createClient(redis_url, options)`
* `redis.createClient(port, host, options)`
* `redis.createClient([options])`
* `redis.createClient(unix_socket[, options])`
* `redis.createClient(redis_url[, options])`
* `redis.createClient(port[, host][, options])`
#### `options` is an object with the following possible properties:
* `host`: *127.0.0.1*; The host to connect to
@ -200,10 +205,10 @@ This delay normally grows infinitely, but setting `retry_max_delay` limits it to
* `connect_timeout`: *3600000*; Setting `connect_timeout` limits total time for client to connect and reconnect.
The value is provided in milliseconds and is counted from the moment on a new client is created / a connection is lost. The last retry is going to happen exactly at the timeout time.
Default is to try connecting until the default system socket timeout has been exceeded and to try reconnecting until 1h passed.
* `max_attempts`: *0*; By default client will try reconnecting until connected. Setting `max_attempts`
* `max_attempts`: *0*; (Deprecated, please use `retry_strategy` instead) By default client will try reconnecting until connected. Setting `max_attempts`
limits total amount of connection tries. Setting this to 1 will prevent any reconnect tries.
* `retry_unfulfilled_commands`: *false*; If set to true, all commands that were unfulfulled while the connection is lost will be retried after the connection has reestablished again. Use this with caution, if you use state altering commands (e.g. *incr*). This is especially useful if you use blocking commands.
* `password`: *null*; If set, client will run redis auth command on connect. Alias `auth_pass`
* `password`: *null*; If set, client will run redis auth command on connect. Alias `auth_pass` (node_redis < 2.5 have to use auth_pass)
* `db`: *null*; If set, client will run redis select command on connect. This is [not recommended](https://groups.google.com/forum/#!topic/redis-db/vS5wX8X4Cjg).
* `family`: *IPv4*; You can force using IPv6 if you set the family to 'IPv6'. See Node.js [net](https://nodejs.org/api/net.html) or [dns](https://nodejs.org/api/dns.html) modules how to use the family type.
* `disable_resubscribing`: *false*; If set to `true`, a client won't resubscribe after disconnecting
@ -211,10 +216,11 @@ limits total amount of connection tries. Setting this to 1 will prevent any reco
* `tls`: an object containing options to pass to [tls.connect](http://nodejs.org/api/tls.html#tls_tls_connect_port_host_options_callback),
to set up a TLS connection to Redis (if, for example, it is set up to be accessible via a tunnel).
* `prefix`: *null*; pass a string to prefix all used keys with that string as prefix e.g. 'namespace:test'
* `retry_strategy`: *function*; pass a function that receives a options object as parameter including the retry `attempt`, the `total_retry_time` indicating how much time passed since the last time connected, the `error` why the connection was lost and the number of `times_connected` in total. If you return a number from this function, the retry will happen exactly after that time in milliseconds. If you return a non-number no further retry is going to happen and all offline commands are flushed with errors. Return a error to return that specific error to all offline commands. Check out the example too.
```js
var redis = require("redis"),
client = redis.createClient({detect_buffers: true});
var redis = require("redis");
var client = redis.createClient({detect_buffers: true});
client.set("foo_rand000000000000", "OK");
@ -230,6 +236,28 @@ client.get(new Buffer("foo_rand000000000000"), function (err, reply) {
client.end();
```
retry_strategy example
```js
var client = redis.createClient({
retry_strategy: function (options) {
if (options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with a individual error
return new Error('The server refused the connection');
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands with a individual error
return new Error('Retry time exhausted');
}
if (options.times_connected > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.max(options.attempt * 100, 3000);
}
});
```
## client.auth(password[, callback])
When connecting to a Redis server that requires authentication, the `AUTH` command must be sent as the
@ -241,6 +269,13 @@ NOTE: Your call to `client.auth()` should not be inside the ready handler. If
you are doing this wrong, `client` will emit an error that looks
something like this `Error: Ready check failed: ERR operation not permitted`.
## backpressure
### stream
The client exposed the used [stream](https://nodejs.org/api/stream.html) in `client.stream` and if the stream or client had to [buffer](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback) the command in `client.should_buffer`.
In combination this can be used to implement backpressure by checking the buffer state before sending a command and listening to the stream [drain](https://nodejs.org/api/stream.html#stream_event_drain) event.
## client.end(flush)
Forcibly close the connection to the Redis server. Note that this does not wait until all replies have been parsed.
@ -267,7 +302,7 @@ client.get("foo_rand000000000000", function (err, reply) {
});
```
`client.end()` without the flush parameter should not be used in production!
`client.end()` without the flush parameter should NOT be used in production!
## client.unref()

91
benchmarks/buffer_bench.js

@ -1,91 +0,0 @@
'use strict';
var source = new Buffer(100),
dest = new Buffer(100), i, j, k, tmp, count = 1000000, bytes = 100;
for (i = 99 ; i >= 0 ; i--) {
source[i] = 120;
}
var str = 'This is a nice String.',
buf = new Buffer('This is a lovely Buffer.');
var start = new Date();
for (i = count * 100; i > 0 ; i--) {
if (Buffer.isBuffer(str)) {}
}
var end = new Date();
console.log('Buffer.isBuffer(str) ' + (end - start) + ' ms');
var start = new Date();
for (i = count * 100; i > 0 ; i--) {
if (Buffer.isBuffer(buf)) {}
}
var end = new Date();
console.log('Buffer.isBuffer(buf) ' + (end - start) + ' ms');
var start = new Date();
for (i = count * 100; i > 0 ; i--) {
if (str instanceof Buffer) {}
}
var end = new Date();
console.log('str instanceof Buffer ' + (end - start) + ' ms');
var start = new Date();
for (i = count * 100; i > 0 ; i--) {
if (buf instanceof Buffer) {}
}
var end = new Date();
console.log('buf instanceof Buffer ' + (end - start) + ' ms');
for (i = bytes ; i > 0 ; i --) {
var start = new Date();
for (j = count ; j > 0; j--) {
tmp = source.toString('ascii', 0, bytes);
}
var end = new Date();
console.log('toString() ' + i + ' bytes ' + (end - start) + ' ms');
}
for (i = bytes ; i > 0 ; i --) {
var start = new Date();
for (j = count ; j > 0; j--) {
tmp = '';
for (k = 0; k <= i ; k++) {
tmp += String.fromCharCode(source[k]);
}
}
var end = new Date();
console.log('manual string ' + i + ' bytes ' + (end - start) + ' ms');
}
for (i = bytes ; i > 0 ; i--) {
var start = new Date();
for (j = count ; j > 0 ; j--) {
for (k = i ; k > 0 ; k--) {
dest[k] = source[k];
}
}
var end = new Date();
console.log('Manual copy ' + i + ' bytes ' + (end - start) + ' ms');
}
for (i = bytes ; i > 0 ; i--) {
var start = new Date();
for (j = count ; j > 0 ; j--) {
for (k = i ; k > 0 ; k--) {
dest[k] = 120;
}
}
var end = new Date();
console.log('Direct assignment ' + i + ' bytes ' + (end - start) + ' ms');
}
for (i = bytes ; i > 0 ; i--) {
var start = new Date();
for (j = count ; j > 0 ; j--) {
source.copy(dest, 0, 0, i);
}
var end = new Date();
console.log('Buffer.copy() ' + i + ' bytes ' + (end - start) + ' ms');
}

58
benchmarks/multi_bench.js

@ -25,9 +25,8 @@ var num_clients = returnArg('clients', 1);
var run_time = returnArg('time', 2500); // ms
var versions_logged = false;
var client_options = {
return_buffers: false,
max_attempts: 4,
parser: returnArg('parser', 'hiredis')
parser: returnArg('parser', 'hiredis'),
path: returnArg('socket') // '/tmp/redis.sock'
};
var small_str, large_str, small_buf, large_buf, very_large_str, very_large_buf;
@ -42,7 +41,7 @@ function lpad(input, len, chr) {
metrics.Histogram.prototype.print_line = function () {
var obj = this.printObj();
return lpad(obj.min, 4) + '/' + lpad(obj.max, 4) + '/' + lpad(obj.mean.toFixed(2), 7);
return lpad((obj.min / 1e6).toFixed(2), 6) + '/' + lpad((obj.max / 1e6).toFixed(2), 6) + '/' + lpad((obj.mean / 1e6).toFixed(2), 6);
};
function Test(args) {
@ -54,7 +53,12 @@ function Test(args) {
this.commands_completed = 0;
this.max_pipeline = this.args.pipeline || 50;
this.batch_pipeline = this.args.batch || 0;
this.client_options = args.client_options || client_options;
this.client_options = args.client_options || {};
this.client_options.parser = client_options.parser;
this.client_options.connect_timeout = 1000;
if (client_options.path) {
this.client_options.path = client_options.path;
}
this.connect_latency = new metrics.Histogram();
this.ready_latency = new metrics.Histogram();
this.command_latency = new metrics.Histogram();
@ -80,8 +84,13 @@ Test.prototype.new_client = function (id) {
new_client.on('ready', function () {
if (!versions_logged) {
console.log('Client count: ' + num_clients + ', node version: ' + process.versions.node + ', server version: ' +
new_client.server_info.redis_version + ', parser: ' + new_client.reply_parser.name);
console.log(
'clients: ' + num_clients +
', NodeJS: ' + process.versions.node +
', Redis: ' + new_client.server_info.redis_version +
', parser: ' + client_options.parser +
', connected by: ' + (client_options.path ? 'socket' : 'tcp')
);
versions_logged = true;
}
self.ready_latency.update(Date.now() - new_client.create_time);
@ -131,14 +140,6 @@ Test.prototype.fill_pipeline = function () {
return;
}
if (this.clients[0].should_buffer) {
var self = this;
setTimeout(function() {
self.fill_pipeline();
}, 1);
return;
}
if (this.batch_pipeline) {
this.batch();
} else {
@ -153,7 +154,7 @@ Test.prototype.fill_pipeline = function () {
Test.prototype.batch = function () {
var self = this,
cur_client = client_nr++ % this.clients.length,
start = Date.now(),
start = process.hrtime(),
i = 0,
batch = this.clients[cur_client].batch();
@ -167,7 +168,7 @@ Test.prototype.batch = function () {
throw err;
}
self.commands_completed += res.length;
self.command_latency.update(Date.now() - start);
self.command_latency.update(process.hrtime(start)[1]);
self.fill_pipeline();
});
};
@ -189,14 +190,14 @@ Test.prototype.stop_clients = function () {
Test.prototype.send_next = function () {
var self = this,
cur_client = this.commands_sent % this.clients.length,
start = Date.now();
start = process.hrtime();
this.clients[cur_client][this.args.command](this.args.args, function (err, res) {
if (err) {
throw err;
}
self.commands_completed++;
self.command_latency.update(Date.now() - start);
self.command_latency.update(process.hrtime(start)[1]);
self.fill_pipeline();
});
};
@ -206,7 +207,7 @@ Test.prototype.print_stats = function () {
totalTime += duration;
console.log('min/max/avg: ' + this.command_latency.print_line() + ' ' + lpad(duration, 6) + 'ms total, ' +
lpad((this.commands_completed / (duration / 1000)).toFixed(2), 9) + ' ops/sec');
lpad(Math.round(this.commands_completed / (duration / 1000)), 7) + ' ops/sec');
};
small_str = '1234';
@ -217,71 +218,54 @@ very_large_str = (new Array((4 * 1024 * 1024) + 1).join('-'));
very_large_buf = new Buffer(very_large_str);
tests.push(new Test({descr: 'PING', command: 'ping', args: [], pipeline: 1}));
// tests.push(new Test({descr: 'PING', command: 'ping', args: [], pipeline: 50}));
tests.push(new Test({descr: 'PING', command: 'ping', args: [], batch: 50}));
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], pipeline: 1}));
// tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], pipeline: 50}));
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], batch: 50}));
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], pipeline: 1}));
// tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], pipeline: 50}));
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], batch: 50}));
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], pipeline: 1}));
// tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], pipeline: 50}));
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], batch: 50}));
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], pipeline: 1, client_opts: { return_buffers: true} }));
// tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], pipeline: 50, client_opts: { return_buffers: true} }));
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], batch: 50, client_opts: { return_buffers: true} }));
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], pipeline: 1}));
// tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], pipeline: 50}));
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], batch: 50}));
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], pipeline: 1}));
// tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], pipeline: 50}));
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], batch: 50}));
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], pipeline: 1}));
// tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], pipeline: 50}));
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], batch: 50}));
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], pipeline: 1, client_opts: { return_buffers: true} }));
// tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], pipeline: 50, client_opts: { return_buffers: true} }));
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], batch: 50, client_opts: { return_buffers: true} }));
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], pipeline: 1}));
// tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], pipeline: 50}));
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], batch: 50}));
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], pipeline: 1}));
// tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], pipeline: 50}));
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], batch: 50}));
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], pipeline: 1}));
// tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], pipeline: 50}));
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], batch: 50}));
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], pipeline: 1}));
// tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], pipeline: 50}));
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], batch: 50}));
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], pipeline: 1}));
// tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], pipeline: 20}));
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], batch: 20}));
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], pipeline: 1}));
// tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], pipeline: 20}));
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], batch: 20}));
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], pipeline: 1}));
// tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], pipeline: 20}));
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], batch: 20}));
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], pipeline: 1, client_opts: { return_buffers: true} }));
// tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], pipeline: 20, client_opts: { return_buffers: true} }));
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], batch: 20, client_opts: { return_buffers: true} }));
function next() {

33
changelog.md

@ -1,18 +1,35 @@
Changelog
=========
## v.2.5.0-0 - xx Dez, 2015
## v.2.5.0-1 - 01 Mar, 2015
This is a big release with some substaintual underlining changes. Therefor this is released as a pre-release and I encourage anyone who's able to, to test this out.
It took way to long to release this one and the next release cycles will be shorter again.
This release is also going to deprecate a couple things to prepare for a future v.3 (it'll still take a while to v.3).
Features
- The parsers moved into the [redis-parser](https://github.com/NodeRedis/node-redis-parser) module and will be maintained in there from now on
- Improve js parser speed significantly for big SUNION/SINTER/LRANGE/ZRANGE
- Improve redis-url parsing to also accept the database-number and options as query parameters as suggested in the [IANA](http://www.iana.org/assignments/uri-schemes/prov/redis)
- Improve redis-url parsing to also accept the database-number and options as query parameters as suggested in [IANA](http://www.iana.org/assignments/uri-schemes/prov/redis)
- Added a `retry_unfulfilled_commands` option
- Setting this to 'true' results in retrying all commands that were not fulfilled on a connection loss after the reconnect. Use with caution
- Added a `db` option to select the database while connecting (this is [not recommended](https://groups.google.com/forum/#!topic/redis-db/vS5wX8X4Cjg))
- Added a `password` option as alias for auth_pass
- The client.server_info is from now on updated while using the info command
- Gracefuly handle redis protocol errors from now on
- Added a `warning` emitter that receives node_redis warnings like auth not required and deprecation messages
- Added a `retry_strategy` option that replaces all reconnect options
- The reconnecting event from now on also receives:
- The error message why the reconnect happend (params.error)
- The amount of times the client was connected (params.times_connected)
- The total reconnecting time since the last time connected (params.total_retry_time)
- Always respect the command execution order no matter if the reply could be returned sync or not (former exceptions: [#937](https://github.com/NodeRedis/node_redis/issues/937#issuecomment-167525939))
- redis.createClient is now checking input values stricter and detects more faulty input
- Started refactoring internals into individual modules
- Pipelining speed improvements
Bugfixes
@ -24,6 +41,9 @@ Bugfixes
- Fixed redis url not accepting the protocol being omitted or protocols other than the redis protocol for convienence
- Fixed parsing the db keyspace even if the first database does not begin with a zero
- Fixed handling of errors occuring while receiving pub sub messages
- Fixed huge string pipelines crashing NodeJS (Pipeline size above 256mb)
- Fixed rename_commands and prefix option not working together
- Fixed ready being emitted to early in case a slave is still syncing / master down
Deprecations
@ -31,15 +51,20 @@ Deprecations
- From v.3.0.0 on using a command with such an argument will return an error instead
- If you want to keep the old behavior please use a precheck in your code that converts the arguments to a string.
- Using SET or SETEX with a undefined or null value will from now on also result in converting the value to "null" / "undefined" to have a consistent behavior. This is not considered as breaking change, as it returned an error earlier.
- Using .end(flush) without the flush parameter deprecated and the flush parameter should explicitly be used
- Using .end(flush) without the flush parameter is deprecated and the flush parameter should explicitly be used
- From v.3.0.0 on using .end without flush will result in an error
- Using .end without flush means that any command that did not yet return is going to silently fail. Therefor this is considered harmfull and you should explicitly silence such errors if you are sure you want this
- Depending on the return value of a command to detect the backpressure is deprecated
- From version 3.0.0 on node_redis might not return true / false as a return value anymore. Please rely on client.should_buffer instead
- The socket_nodelay option is deprecated and will be removed in v.3.0.0
- The `socket_nodelay` option is deprecated and will be removed in v.3.0.0
- If you want to buffer commands you should use [.batch or .multi](./README.md) instead. This is necessary to reduce the amount of different options and this is very likely reducing your throughput if set to false.
- If you are sure you want to activate the NAGLE algorithm you can still activate it by using client.stream.setNoDelay(false)
- The `max_attempts` option is deprecated and will be removed in v.3.0.0. Please use the `retry_strategy` instead
- The `retry_max_delay` option is deprecated and will be removed in v.3.0.0. Please use the `retry_strategy` instead
- The drain event is deprecated and will be removed in v.3.0.0. Please listen to the stream drain event instead
- The idle event is deprecated and will likely be removed in v.3.0.0. If you rely on this feature please open a new ticket in node_redis with your use case
- Redis < v. 2.6.11 is not supported anymore and will not work in all cases. Please update to a newer redis version
- Removed non documented command syntax (adding the callback to an arguments array instead of passing it as individual argument)
## v.2.4.2 - 27 Nov, 2015

929
index.js

File diff suppressed because it is too large

4
lib/command.js

@ -2,10 +2,10 @@
// This Command constructor is ever so slightly faster than using an object literal, but more importantly, using
// a named constructor helps it show up meaningfully in the V8 CPU profiler and in heap snapshots.
function Command(command, args, buffer_args, callback) {
function Command(command, args, callback) {
this.command = command;
this.args = args;
this.buffer_args = buffer_args;
this.buffer_args = false;
this.callback = callback;
}

86
lib/commands.js

@ -0,0 +1,86 @@
'use strict';
var commands = require('redis-commands');
var Multi = require('./multi');
var RedisClient = require('../').RedisClient;
// TODO: Rewrite this including the invidual commands into a Commands class
// that provided a functionality to add new commands to the client
commands.list.forEach(function (command) {
// Do not override existing functions
if (!RedisClient.prototype[command]) {
RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command] = function () {
var arr;
var len = arguments.length;
var callback;
var i = 0;
if (Array.isArray(arguments[0])) {
arr = arguments[0];
if (len === 2) {
callback = arguments[1];
}
} else if (len > 1 && Array.isArray(arguments[1])) {
if (len === 3) {
callback = arguments[2];
}
len = arguments[1].length;
arr = new Array(len + 1);
arr[0] = arguments[0];
for (; i < len; i += 1) {
arr[i + 1] = arguments[1][i];
}
} else {
// The later should not be the average use case
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
len--;
callback = arguments[len];
}
arr = new Array(len);
for (; i < len; i += 1) {
arr[i] = arguments[i];
}
}
return this.send_command(command, arr, callback);
};
}
// Do not override existing functions
if (!Multi.prototype[command]) {
Multi.prototype[command.toUpperCase()] = Multi.prototype[command] = function () {
var arr;
var len = arguments.length;
var callback;
var i = 0;
if (Array.isArray(arguments[0])) {
arr = arguments[0];
if (len === 2) {
callback = arguments[1];
}
} else if (len > 1 && Array.isArray(arguments[1])) {
if (len === 3) {
callback = arguments[2];
}
len = arguments[1].length;
arr = new Array(len + 1);
arr[0] = arguments[0];
for (; i < len; i += 1) {
arr[i + 1] = arguments[1][i];
}
} else {
// The later should not be the average use case
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
len--;
callback = arguments[len];
}
arr = new Array(len);
for (; i < len; i += 1) {
arr[i] = arguments[i];
}
}
this.queue.push([command, arr, callback]);
return this;
};
}
});

79
lib/createClient.js

@ -0,0 +1,79 @@
'use strict';
var utils = require('./utils');
var URL = require('url');
module.exports = function createClient (port_arg, host_arg, options) {
if (typeof port_arg === 'number' || typeof port_arg === 'string' && /^\d+$/.test(port_arg)) {
var host;
if (typeof host_arg === 'string') {
host = host_arg;
} else {
if (options && host_arg) {
throw new Error('Unknown type of connection in createClient()');
}
options = options || host_arg;
}
options = utils.clone(options);
options.host = host || options.host;
options.port = port_arg;
} else if (typeof port_arg === 'string' || port_arg && port_arg.url) {
options = utils.clone(port_arg.url ? port_arg : host_arg || options);
var parsed = URL.parse(port_arg.url || port_arg, true, true);
// [redis:]//[[user][:password]@][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]]
if (parsed.slashes) { // We require slashes
if (parsed.auth) {
options.password = parsed.auth.split(':')[1];
}
if (parsed.protocol && parsed.protocol !== 'redis:') {
console.warn('node_redis: WARNING: You passed "' + parsed.protocol.substring(0, parsed.protocol.length - 1) + '" as protocol instead of the "redis" protocol!');
}
if (parsed.pathname && parsed.pathname !== '/') {
options.db = parsed.pathname.substr(1);
}
if (parsed.hostname) {
options.host = parsed.hostname;
}
if (parsed.port) {
options.port = parsed.port;
}
if (parsed.search !== '') {
var elem;
for (elem in parsed.query) { // jshint ignore: line
// If options are passed twice, only the parsed options will be used
if (elem in options) {
if (options[elem] === parsed.query[elem]) {
console.warn('node_redis: WARNING: You passed the ' + elem + ' option twice!');
} else {
throw new Error('The ' + elem + ' option is added twice and does not match');
}
}
options[elem] = parsed.query[elem];
}
}
} else if (parsed.hostname) {
throw new Error('The redis url must begin with slashes "//" or contain slashes after the redis protocol');
} else {
options.path = port_arg;
}
} else if (typeof port_arg === 'object' || port_arg === undefined) {
options = utils.clone(port_arg || options);
options.host = options.host || host_arg;
if (port_arg && arguments.length !== 1) {
throw new Error('To many arguments passed to createClient. Please only pass the options object');
}
}
if (!options) {
throw new Error('Unknown type of connection in createClient()');
}
return options;
};

11
lib/debug.js

@ -0,0 +1,11 @@
'use strict';
var index = require('../');
function debug (msg) {
if (index.debug_mode) {
console.error(msg);
}
}
module.exports = debug;

137
lib/individualCommands.js

@ -0,0 +1,137 @@
'use strict';
var utils = require('./utils');
var debug = require('./debug');
var Multi = require('./multi');
var no_password_is_set = /no password is set/;
var RedisClient = require('../').RedisClient;
/********************************
Replace built-in redis functions
********************************/
RedisClient.prototype.multi = RedisClient.prototype.MULTI = function multi (args) {
var multi = new Multi(this, args);
multi.exec = multi.EXEC = multi.exec_transaction;
return multi;
};
// ATTENTION: This is not a native function but is still handled as a individual command as it behaves just the same as multi
RedisClient.prototype.batch = RedisClient.prototype.BATCH = function batch (args) {
return new Multi(this, args);
};
// Store db in this.select_db to restore it on reconnect
RedisClient.prototype.select = RedisClient.prototype.SELECT = function select (db, callback) {
var self = this;
return this.send_command('select', [db], function (err, res) {
if (err === null) {
self.selected_db = db;
}
utils.callback_or_emit(self, callback, err, res);
});
};
// Store info in this.server_info after each call
RedisClient.prototype.info = RedisClient.prototype.INFO = function info (callback) {
var self = this;
var ready = this.ready;
this.ready = ready || this.offline_queue.length === 0; // keep the execution order intakt
var tmp = this.send_command('info', [], function (err, res) {
if (res) {
var obj = {};
var lines = res.toString().split('\r\n');
var line, parts, sub_parts;
for (var i = 0; i < lines.length; i++) {
parts = lines[i].split(':');
if (parts[1]) {
if (parts[0].indexOf('db') === 0) {
sub_parts = parts[1].split(',');
obj[parts[0]] = {};
while (line = sub_parts.pop()) {
line = line.split('=');
obj[parts[0]][line[0]] = +line[1];
}
} else {
obj[parts[0]] = parts[1];
}
}
}
obj.versions = [];
/* istanbul ignore else: some redis servers do not send the version */
if (obj.redis_version) {
obj.redis_version.split('.').forEach(function (num) {
obj.versions.push(+num);
});
}
// Expose info key/vals to users
self.server_info = obj;
} else {
self.server_info = {};
}
utils.callback_or_emit(self, callback, err, res);
});
this.ready = ready;
return tmp;
};
RedisClient.prototype.auth = RedisClient.prototype.AUTH = function auth (pass, callback) {
var self = this;
var ready = this.ready;
debug('Sending auth to ' + self.address + ' id ' + self.connection_id);
// Stash auth for connect and reconnect.
this.auth_pass = pass;
this.ready = this.offline_queue.length === 0; // keep the execution order intakt
var tmp = this.send_command('auth', [pass], function (err, res) {
if (err && no_password_is_set.test(err.message)) {
self.warn('Warning: Redis server does not require a password, but a password was supplied.');
err = null;
res = 'OK';
}
utils.callback_or_emit(self, callback, err, res);
});
this.ready = ready;
return tmp;
};
RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function hmset () {
var arr,
len = arguments.length,
callback,
i = 0;
if (Array.isArray(arguments[0])) {
arr = arguments[0];
callback = arguments[1];
} else if (Array.isArray(arguments[1])) {
if (len === 3) {
callback = arguments[2];
}
len = arguments[1].length;
arr = new Array(len + 1);
arr[0] = arguments[0];
for (; i < len; i += 1) {
arr[i + 1] = arguments[1][i];
}
} else if (typeof arguments[1] === 'object' && (arguments.length === 2 || arguments.length === 3 && typeof arguments[2] === 'function' || typeof arguments[2] === 'undefined')) {
arr = [arguments[0]];
for (var field in arguments[1]) { // jshint ignore: line
arr.push(field, arguments[1][field]);
}
callback = arguments[2];
} else {
len = arguments.length;
// The later should not be the average use case
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
len--;
callback = arguments[len];
}
arr = new Array(len);
for (; i < len; i += 1) {
arr[i] = arguments[i];
}
}
return this.send_command('hmset', arr, callback);
};

224
lib/multi.js

@ -0,0 +1,224 @@
'use strict';
var Queue = require('double-ended-queue');
var utils = require('./utils');
function Multi(client, args) {
this._client = client;
this.queue = new Queue();
var command, tmp_args;
if (args) { // Either undefined or an array. Fail hard if it's not an array
for (var i = 0; i < args.length; i++) {
command = args[i][0];
tmp_args = args[i].slice(1);
if (Array.isArray(command)) {
this[command[0]].apply(this, command.slice(1).concat(tmp_args));
} else {
this[command].apply(this, tmp_args);
}
}
}
}
Multi.prototype.hmset = Multi.prototype.HMSET = function hmset () {
var arr,
len = 0,
callback,
i = 0;
if (Array.isArray(arguments[0])) {
arr = arguments[0];
callback = arguments[1];
} else if (Array.isArray(arguments[1])) {
len = arguments[1].length;
arr = new Array(len + 1);
arr[0] = arguments[0];
for (; i < len; i += 1) {
arr[i + 1] = arguments[1][i];
}
callback = arguments[2];
} else if (typeof arguments[1] === 'object' && (typeof arguments[2] === 'function' || typeof arguments[2] === 'undefined')) {
arr = [arguments[0]];
for (var field in arguments[1]) { // jshint ignore: line
arr.push(field, arguments[1][field]);
}
callback = arguments[2];
} else {
len = arguments.length;
// The later should not be the average use case
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
len--;
callback = arguments[len];
}
arr = new Array(len);
for (; i < len; i += 1) {
arr[i] = arguments[i];
}
}
this.queue.push(['hmset', arr, callback]);
return this;
};
function pipeline_transaction_command (self, command, args, index, cb) {
self._client.send_command(command, args, function (err, reply) {
if (err) {
if (cb) {
cb(err);
}
err.position = index;
self.errors.push(err);
}
});
}
Multi.prototype.exec_atomic = function exec_atomic (callback) {
if (this.queue.length < 2) {
return this.exec_batch(callback);
}
return this.exec(callback);
};
function multi_callback (self, err, replies) {
var i = 0, args;
if (err) {
// The errors would be circular
var connection_error = ['CONNECTION_BROKEN', 'UNCERTAIN_STATE'].indexOf(err.code) !== -1;
err.errors = connection_error ? [] : self.errors;
if (self.callback) {
self.callback(err);
// Exclude connection errors so that those errors won't be emitted twice
} else if (!connection_error) {
self._client.emit('error', err);
}
return;
}
if (replies) {
while (args = self.queue.shift()) {
if (replies[i] instanceof Error) {
var match = replies[i].message.match(utils.err_code);
// LUA script could return user errors that don't behave like all other errors!
if (match) {
replies[i].code = match[1];
}
replies[i].command = args[0].toUpperCase();
if (typeof args[2] === 'function') {
args[2](replies[i]);
}
} else {
// If we asked for strings, even in detect_buffers mode, then return strings:
replies[i] = self._client.handle_reply(replies[i], args[0], self.wants_buffers[i]);
if (typeof args[2] === 'function') {
args[2](null, replies[i]);
}
}
i++;
}
}
if (self.callback) {
self.callback(null, replies);
}
}
Multi.prototype.exec_transaction = function exec_transaction (callback) {
var self = this;
var len = self.queue.length;
self.errors = [];
self.callback = callback;
self._client.cork(len + 2);
self.wants_buffers = new Array(len);
pipeline_transaction_command(self, 'multi', []);
// Drain queue, callback will catch 'QUEUED' or error
for (var index = 0; index < len; index++) {
var args = self.queue.get(index);
var command = args[0];
var cb = args[2];
// Keep track of who wants buffer responses:
if (self._client.options.detect_buffers) {
self.wants_buffers[index] = false;
for (var i = 0; i < args[1].length; i += 1) {
if (args[1][i] instanceof Buffer) {
self.wants_buffers[index] = true;
break;
}
}
}
pipeline_transaction_command(self, command, args[1], index, cb);
}
self._client.send_command('exec', [], function(err, replies) {
multi_callback(self, err, replies);
});
self._client.uncork();
self._client.writeDefault = self._client.writeStrings;
return !self._client.should_buffer;
};
function batch_callback (self, cb, i) {
return function batch_callback (err, res) {
if (err) {
self.results[i] = err;
// Add the position to the error
self.results[i].position = i;
} else {
self.results[i] = res;
}
cb(err, res);
};
}
Multi.prototype.exec = Multi.prototype.EXEC = Multi.prototype.exec_batch = function exec_batch (callback) {
var self = this;
var len = self.queue.length;
var index = 0;
var args;
var args_len = 1;
var callback_without_own_cb = function (err, res) {
if (err) {
self.results.push(err);
// Add the position to the error
var i = self.results.length - 1;
self.results[i].position = i;
} else {
self.results.push(res);
}
// Do not emit an error here. Otherwise each error would result in one emit.
// The errors will be returned in the result anyway
};
var last_callback = function (cb) {
return function (err, res) {
cb(err, res);
callback(null, self.results);
};
};
if (len === 0) {
if (callback) {
utils.reply_in_order(self._client, callback, null, []);
}
return true;
}
self.results = [];
self._client.cork(len);
while (args = self.queue.shift()) {
var command = args[0];
var cb;
args_len = args[1].length - 1;
if (typeof args[2] === 'function') {
cb = batch_callback(self, args[2], index);
} else {
cb = callback_without_own_cb;
}
if (callback && index === len - 1) {
cb = last_callback(cb);
}
self._client.send_command(command, args[1], cb);
index++;
}
self.queue = new Queue();
self._client.uncork();
self._client.writeDefault = self._client.writeStrings;
return !self._client.should_buffer;
};
module.exports = Multi;

89
lib/utils.js

@ -1,32 +1,26 @@
'use strict';
// hgetall converts its replies to an Object. If the reply is empty, null is returned.
// These function are only called with internal data and have therefor always the same instanceof X
function replyToObject(reply) {
var obj = {}, j, jl, key, val;
if (reply.length === 0 || !Array.isArray(reply)) {
// The reply might be a string or a buffer if this is called in a transaction (multi)
if (reply.length === 0 || !(reply instanceof Array)) {
return null;
}
for (j = 0, jl = reply.length; j < jl; j += 2) {
key = reply[j].toString('binary');
val = reply[j + 1];
obj[key] = val;
var obj = {};
for (var i = 0; i < reply.length; i += 2) {
obj[reply[i].toString('binary')] = reply[i + 1];
}
return obj;
}
function replyToStrings(reply) {
var i;
if (Buffer.isBuffer(reply)) {
if (reply instanceof Buffer) {
return reply.toString();
}
if (Array.isArray(reply)) {
if (reply instanceof Array) {
var res = new Array(reply.length);
for (i = 0; i < reply.length; i++) {
for (var i = 0; i < reply.length; i++) {
// Recusivly call the function as slowlog returns deep nested replies
res[i] = replyToStrings(reply[i]);
}
@ -38,7 +32,8 @@ function replyToStrings(reply) {
function print (err, reply) {
if (err) {
console.log('Error: ' + err);
// A error always begins with Error:
console.log(err.toString());
} else {
console.log('Reply: ' + reply);
}
@ -46,9 +41,69 @@ function print (err, reply) {
var redisErrCode = /^([A-Z]+)\s+(.+)$/;
// Deep clone arbitrary objects with arrays. Can't handle cyclic structures (results in a range error)
// Any attribute with a non primitive value besides object and array will be passed by reference (e.g. Buffers, Maps, Functions)
function clone (obj) {
var copy;
if (Array.isArray(obj)) {
copy = new Array(obj.length);
for (var i = 0; i < obj.length; i++) {
copy[i] = clone(obj[i]);
}
return copy;
}
if (Object.prototype.toString.call(obj) === '[object Object]') {
copy = {};
var elems = Object.keys(obj);
var elem;
while (elem = elems.pop()) {
copy[elem] = clone(obj[elem]);
}
return copy;
}
return obj;
}
function convenienceClone (obj) {
return clone(obj) || {};
}
function callbackOrEmit (self, callback, err, res) {
if (callback) {
callback(err, res);
} else if (err) {
self.emit('error', err);
}
}
function replyInOrder (self, callback, err, res) {
var command_obj = self.command_queue.peekBack() || self.offline_queue.peekBack();
if (!command_obj) {
process.nextTick(function () {
callbackOrEmit(self, callback, err, res);
});
} else {
var tmp = command_obj.callback;
command_obj.callback = tmp ?
function (e, r) {
tmp(e, r);
callbackOrEmit(self, callback, err, res);
} :
function (e, r) {
if (e) {
self.emit('error', e);
}
callbackOrEmit(self, callback, err, res);
};
}
}
module.exports = {
reply_to_strings: replyToStrings,
reply_to_object: replyToObject,
print: print,
err_code: redisErrCode
err_code: redisErrCode,
clone: convenienceClone,
callback_or_emit: callbackOrEmit,
reply_in_order: replyInOrder
};

3
package.json

@ -25,7 +25,7 @@
"dependencies": {
"double-ended-queue": "^2.1.0-0",
"redis-commands": "^1.0.1",
"redis-parser": "^1.0.0"
"redis-parser": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
@ -33,6 +33,7 @@
"devDependencies": {
"bluebird": "^3.0.2",
"coveralls": "^2.11.2",
"intercept-stdout": "~0.1.2",
"jshint": "^2.8.0",
"metrics": "^0.1.9",
"mocha": "^2.3.2",

10
test/auth.spec.js

@ -5,6 +5,11 @@ var config = require("./lib/config");
var helper = require('./helper');
var redis = config.redis;
if (process.platform === 'win32') {
// TODO: Fix redis process spawn on windows
return;
}
describe("client authentication", function () {
before(function (done) {
helper.stopRedis(function () {
@ -161,7 +166,7 @@ describe("client authentication", function () {
client = redis.createClient.apply(redis.createClient, args);
var async = true;
client.auth(undefined, function(err, res) {
assert.strictEqual(err.message, 'The password has to be of type "string"');
assert.strictEqual(err.message, 'ERR invalid password');
assert.strictEqual(err.command, 'AUTH');
assert.strictEqual(res, undefined);
async = false;
@ -175,7 +180,7 @@ describe("client authentication", function () {
client = redis.createClient.apply(redis.createClient, args);
client.on('error', function (err) {
assert.strictEqual(err.message, 'The password has to be of type "string"');
assert.strictEqual(err.message, 'ERR invalid password');
assert.strictEqual(err.command, 'AUTH');
done();
});
@ -237,6 +242,7 @@ describe("client authentication", function () {
});
after(function (done) {
if (helper.redisProcess().spawnFailed()) return done();
helper.stopRedis(function () {
helper.startRedis('./conf/redis.conf', done);
});

23
test/batch.spec.js

@ -63,11 +63,16 @@ describe("The 'batch' method", function () {
client.end(true);
});
it("returns an empty array", function (done) {
it("returns an empty array and keep the execution order in takt", function (done) {
var called = false;
client.set('foo', 'bar', function (err, res) {
called = true;
});
var batch = client.batch();
batch.exec(function (err, res) {
assert.strictEqual(err, null);
assert.strictEqual(res.length, 0);
assert(called);
done();
});
});
@ -199,8 +204,8 @@ describe("The 'batch' method", function () {
arr4,
[["mset", "batchfoo2", "batchbar2", "batchfoo3", "batchbar3"], helper.isString('OK')],
["hmset", arr],
[["hmset", "batchhmset2", "batchbar2", "batchfoo3", "batchbar3", "test", helper.isString('OK')]],
["hmset", ["batchhmset", "batchbar", "batchfoo", helper.isString('OK')]],
[["hmset", "batchhmset2", "batchbar2", "batchfoo3", "batchbar3", "test"], helper.isString('OK')],
["hmset", ["batchhmset", "batchbar", "batchfoo"], helper.isString('OK')],
["hmset", arr3, helper.isString('OK')],
['hmset', now, {123456789: "abcdefghij", "some manner of key": "a type of value", "otherTypes": 555}],
['hmset', 'key2', {"0123456789": "abcdefghij", "some manner of key": "a type of value", "otherTypes": 999}, helper.isString('OK')],
@ -211,8 +216,9 @@ describe("The 'batch' method", function () {
.hmget('key2', arr2, function noop() {})
.hmget(['batchhmset2', 'some manner of key', 'batchbar3'])
.mget('batchfoo2', ['batchfoo3', 'batchfoo'], function(err, res) {
assert(res[0], 'batchfoo3');
assert(res[1], 'batchfoo');
assert.strictEqual(res[0], 'batchbar2');
assert.strictEqual(res[1], 'batchbar3');
assert.strictEqual(res[2], null);
})
.exec(function (err, replies) {
assert.equal(arr.length, 3);
@ -273,7 +279,7 @@ describe("The 'batch' method", function () {
it('allows multiple commands to work the same as normal to be performed using a chaining API', function (done) {
client.batch()
.mset(['some', '10', 'keys', '20'])
.incr(['some', helper.isNumber(11)])
.incr('some', helper.isNumber(11))
.incr(['keys'], helper.isNumber(21))
.mget('some', 'keys')
.exec(function (err, replies) {
@ -290,7 +296,7 @@ describe("The 'batch' method", function () {
it('allows multiple commands to work the same as normal to be performed using a chaining API promisified', function () {
return client.batch()
.mset(['some', '10', 'keys', '20'])
.incr(['some', helper.isNumber(11)])
.incr('some', helper.isNumber(11))
.incr(['keys'], helper.isNumber(21))
.mget('some', 'keys')
.execAsync()
@ -327,10 +333,11 @@ describe("The 'batch' method", function () {
.exec(done);
});
it("should work without any callback", function (done) {
it("should work without any callback or arguments", function (done) {
var batch = client.batch();
batch.set("baz", "binary");
batch.set("foo", "bar");
batch.ping();
batch.exec();
client.get('foo', helper.isString('bar', done));

8
test/commands/blpop.spec.js

@ -4,6 +4,7 @@ var assert = require("assert");
var config = require("../lib/config");
var helper = require("../helper");
var redis = config.redis;
var intercept = require('intercept-stdout');
describe("The 'blpop' method", function () {
@ -23,7 +24,14 @@ describe("The 'blpop' method", function () {
it('pops value immediately if list contains values', function (done) {
bclient = redis.createClient.apply(redis.createClient, args);
redis.debug_mode = true;
var text = '';
var unhookIntercept = intercept(function(data) {
text += data;
return '';
});
client.rpush("blocking list", "initial value", helper.isNumber(1));
unhookIntercept();
assert(/^Send 127\.0\.0\.1:6379 id [0-9]+: \*3\r\n\$5\r\nrpush\r\n\$13\r\nblocking list\r\n\$13\r\ninitial value\r\n\n$/.test(text));
redis.debug_mode = false;
bclient.blpop("blocking list", 0, function (err, value) {
assert.strictEqual(value[0], "blocking list");

8
test/commands/get.spec.js

@ -68,20 +68,12 @@ describe("The 'get' method", function () {
});
it("gets the value correctly", function (done) {
client.GET(key, redis.print); // Use the utility function to print the result
client.GET(key, function (err, res) {
helper.isString(value)(err, res);
done(err);
});
});
it("gets the value correctly with array syntax and the callback being in the array", function (done) {
client.GET([key, function (err, res) {
helper.isString(value)(err, res);
done(err);
}]);
});
it("should not throw on a get without callback (even if it's not useful)", function (done) {
client.GET(key);
client.on('error', function(err) {

1
test/commands/getset.spec.js

@ -33,7 +33,6 @@ describe("The 'getset' method", function () {
});
it("reports an error", function (done) {
client.GET(key, redis.print); // Use the utility function to print the error
client.get(key, function (err, res) {
assert(err.message.match(/The connection has already been closed/));
done();

4
test/commands/hgetall.spec.js

@ -64,7 +64,7 @@ describe("The 'hgetall' method", function () {
it('returns binary results', function (done) {
client.hmset(["bhosts", "mjr", "1", "another", "23", "home", "1234", new Buffer([0xAA, 0xBB, 0x00, 0xF0]), new Buffer([0xCC, 0xDD, 0x00, 0xF0])], helper.isString("OK"));
client.HGETALL(["bhosts", function (err, obj) {
client.HGETALL("bhosts", function (err, obj) {
assert.strictEqual(4, Object.keys(obj).length);
assert.strictEqual("1", obj.mjr.toString());
assert.strictEqual("23", obj.another.toString());
@ -72,7 +72,7 @@ describe("The 'hgetall' method", function () {
assert.strictEqual((new Buffer([0xAA, 0xBB, 0x00, 0xF0])).toString('binary'), Object.keys(obj)[3]);
assert.strictEqual((new Buffer([0xCC, 0xDD, 0x00, 0xF0])).toString('binary'), obj[(new Buffer([0xAA, 0xBB, 0x00, 0xF0])).toString('binary')].toString('binary'));
return done(err);
}]);
});
});
});

2
test/commands/mset.spec.js

@ -89,7 +89,7 @@ describe("The 'mset' method", function () {
it("sets the value correctly with array syntax", function (done) {
client.mset([key, value2, key2, value]);
client.get([key, helper.isString(value2)]);
client.get(key, helper.isString(value2));
client.get(key2, helper.isString(value, done));
});
});

11
test/commands/set.spec.js

@ -66,14 +66,21 @@ describe("The 'set' method", function () {
});
});
describe("with undefined 'key' and missing 'value' parameter", function () {
it("reports an error", function (done) {
describe("reports an error with invalid parameters", function () {
it("undefined 'key' and missing 'value' parameter", function (done) {
client.set(undefined, function (err, res) {
helper.isError()(err, null);
assert.equal(err.command, 'SET');
done();
});
});
it("empty array as second parameter", function (done) {
client.set('foo', [], function (err, res) {
assert.strictEqual(err.message, "ERR wrong number of arguments for 'set' command");
done();
});
});
});
});

99
test/conect.slave.spec.js

@ -0,0 +1,99 @@
'use strict';
var assert = require("assert");
var config = require("./lib/config");
var helper = require('./helper');
var RedisProcess = require("./lib/redis-process");
var rp;
var path = require('path');
var redis = config.redis;
if (process.platform === 'win32') {
// TODO: Fix redis process spawn on windows
return;
}
describe('master slave sync', function () {
var master = null;
var slave = null;
before(function (done) {
helper.stopRedis(function () {
helper.startRedis('./conf/password.conf', done);
});
});
before(function (done) {
if (helper.redisProcess().spawnFailed()) return done();
master = redis.createClient({
password: 'porkchopsandwiches'
});
var multi = master.multi();
var i = 0;
while (i < 1000) {
i++;
// Write some data in the redis instance, so there's something to sync
multi.set('foo' + i, 'bar' + new Array(500).join(Math.random()));
}
multi.exec(done);
});
it("sync process and no master should delay ready being emitted for slaves", function (done) {
if (helper.redisProcess().spawnFailed()) this.skip();
var port = 6381;
var firstInfo;
slave = redis.createClient({
port: port,
retry_strategy: function (options) {
// Try to reconnect in very small intervals to catch the master_link_status down before the sync completes
return 10;
}
});
var tmp = slave.info.bind(slave);
var i = 0;
slave.info = function (err, res) {
i++;
tmp(err, res);
if (!firstInfo || Object.keys(firstInfo).length === 0) {
firstInfo = slave.server_info;
}
};
slave.on('connect', function () {
assert.strictEqual(i, 0);
});
var end = helper.callFuncAfter(done, 2);
slave.on('ready', function () {
assert.strictEqual(this.server_info.master_link_status, 'up');
assert.strictEqual(firstInfo.master_link_status, 'down');
assert(i > 1);
this.get('foo300', function (err, res) {
assert.strictEqual(res.substr(0, 3), 'bar');
end(err);
});
});
RedisProcess.start(function (err, _rp) {
rp = _rp;
end(err);
}, path.resolve(__dirname, './conf/slave.conf'), port);
});
after(function (done) {
if (helper.redisProcess().spawnFailed()) return done();
var end = helper.callFuncAfter(done, 3);
rp.stop(end);
slave.end(true);
master.flushdb(function (err) {
end(err);
master.end(true);
});
helper.stopRedis(function () {
helper.startRedis('./conf/redis.conf', end);
});
});
});

19
test/conf/faulty.cert

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDATCCAemgAwIBAgIJALkMmVkQOERnMA0GCSqGSIb3DQEBBQUAMBcxFTATBgNV
BAMMDHJlZGlzLmpzLm9yZzAeFw0xNTEwMTkxMjIzMjRaFw0yNTEwMTYxMjIzMjRa
MBcxFTATBgNVBAMMDHJlZGlzLmpzLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAJ/DmMTJHf7kyspxI1A/JmOc+KI9vxEcN5qn7IiZuGN7ghE43Q3q
XB2GUkMAuW1POkmM5yi3SuT1UXDR/4Gk7KlbHKMs37AV6PgJXX6oX0zu12LTAT7V
5byNrYtehSo42l1188dGEMCGaaf0cDntc7A3aW0ZtzrJt+2pu31Uatl2SEJCMra6
+v6O0c9aHMF1cArKeawGqR+jHw6vXFZQbUd06nW5nQlUA6wVt1JjlLPwBwYsWLsi
YQxMC8NqpgAIg5tULSCpKwx5isL/CeotVVGDNZ/G8R1nTrxuygPlc3Qskj57hmV4
tZK4JJxQFi7/9ehvjAvHohKrEPeqV5XL87cCAwEAAaNQME4wHQYDVR0OBBYEFCn/
5hB+XY4pVOnaqvrmZMxrLFjLMB8GA1UdIwQYMBaAFCn/5hB+XY4pVOnaqvrmZMxr
LFjLMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEduPyTHpXkCVZRQ
v6p+Ug4iVeXpxGCVr34y7EDUMgmuDdqsz1SrmqeDd0VmjZT8htbWw7QBKDPEBsbi
wl606aAn01iM+oUrwbtXxid1xfZj/j6pIhQVkGu7e/8A7Pr4QOP4OMdHB7EmqkAo
d/OLHa9LdKv2UtJHD6U7oVQbdBHrRV62125GMmotpQuSkEfZM6edKNzHPlqV/zJc
2kGCw3lZC21mTrsSMIC/FQiobPnig4kAvfh0of2rK/XAntlwT8ie1v1aK+jERsfm
uzMihl6XXBdzheq6KdIlf+5STHBIIRcvBoRKr5Va7EhnO03tTzeJowtqDv47yPC6
w4kLcP8=
-----END CERTIFICATE-----

6
test/conf/slave.conf

@ -0,0 +1,6 @@
port 6381
bind ::1 127.0.0.1
unixsocket /tmp/redis6381.sock
unixsocketperm 755
slaveof localhost 6379
masterauth porkchopsandwiches

161
test/connection.spec.js

@ -4,6 +4,7 @@ var assert = require("assert");
var config = require("./lib/config");
var helper = require('./helper');
var redis = config.redis;
var intercept = require('intercept-stdout');
describe("connection tests", function () {
helper.allTests(function(parser, ip, args) {
@ -91,6 +92,7 @@ describe("connection tests", function () {
client.on("reconnecting", function (params) {
client.end(true);
assert.strictEqual(params.times_connected, 1);
setTimeout(done, 100);
});
});
@ -127,23 +129,83 @@ describe("connection tests", function () {
client.stream.destroy();
});
});
it("retry_strategy used to reconnect with individual error", function (done) {
var text = '';
var unhookIntercept = intercept(function (data) {
text += data;
return '';
});
var end = helper.callFuncAfter(done, 2);
client = redis.createClient({
retry_strategy: function (options) {
if (options.total_retry_time > 150) {
client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, 'Connection timeout');
end();
});
// Pass a individual error message to the error handler
return new Error('Connection timeout');
}
return Math.min(options.attempt * 25, 200);
},
max_attempts: 5,
retry_max_delay: 123,
port: 9999
});
client.on('error', function(err) {
unhookIntercept();
assert.strictEqual(
text,
'node_redis: WARNING: You activated the retry_strategy and max_attempts at the same time. This is not possible and max_attempts will be ignored.\n' +
'node_redis: WARNING: You activated the retry_strategy and retry_max_delay at the same time. This is not possible and retry_max_delay will be ignored.\n'
);
assert.strictEqual(err.message, 'Connection timeout');
assert(!err.code);
end();
});
});
it("retry_strategy used to reconnect", function (done) {
var end = helper.callFuncAfter(done, 2);
client = redis.createClient({
retry_strategy: function (options) {
if (options.total_retry_time > 150) {
client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.code, 'ECONNREFUSED');
end();
});
return false;
}
return Math.min(options.attempt * 25, 200);
},
port: 9999
});
client.on('error', function(err) {
assert.strictEqual(err.code, 'ECONNREFUSED');
end();
});
});
});
describe("when not connected", function () {
it("emit an error after the socket timeout exceeded the connect_timeout time", function (done) {
var connect_timeout = 1000; // in ms
var time = Date.now();
client = redis.createClient({
parser: parser,
host: '192.168.74.167', // Should be auto detected as ipv4
// Auto detect ipv4 and use non routable ip to trigger the timeout
host: '10.255.255.1',
connect_timeout: connect_timeout
});
process.nextTick(function() {
assert(client.stream._events.timeout);
assert.strictEqual(client.stream.listeners('timeout').length, 1);
});
assert.strictEqual(client.address, '192.168.74.167:6379');
assert.strictEqual(client.address, '10.255.255.1:6379');
assert.strictEqual(client.connection_options.family, 4);
var time = Date.now();
client.on("reconnecting", function (params) {
throw new Error('No reconnect, since no connection was ever established');
@ -151,8 +213,8 @@ describe("connection tests", function () {
client.on('error', function(err) {
assert(/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message));
assert(Date.now() - time < connect_timeout + 50);
assert(Date.now() - time >= connect_timeout);
assert(Date.now() - time < connect_timeout + 25);
assert(Date.now() - time >= connect_timeout - 3); // Timers sometimes trigger early (e.g. 1ms to early)
done();
});
});
@ -165,7 +227,7 @@ describe("connection tests", function () {
assert.strictEqual(client.address, '2001:db8::ff00:42:8329:6379');
assert.strictEqual(client.connection_options.family, 6);
process.nextTick(function() {
assert.strictEqual(client.stream._events.timeout, undefined);
assert.strictEqual(client.stream.listeners('timeout').length, 0);
});
});
@ -179,7 +241,7 @@ describe("connection tests", function () {
});
client.on('connect', function () {
assert.strictEqual(client.stream._idleTimeout, -1);
assert.strictEqual(client.stream._events.timeout, undefined);
assert.strictEqual(client.stream.listeners('timeout').length, 0);
client.on('ready', done);
});
});
@ -192,13 +254,13 @@ describe("connection tests", function () {
connect_timeout: 1000
});
client.once('ready', function() {
done();
});
client.once('ready', done);
});
if (process.platform !== 'win32') {
it("connect with path provided in the options object", function (done) {
if (process.platform === 'win32') {
this.skip();
}
client = redis.createClient({
path: '/tmp/redis.sock',
parser: parser,
@ -207,13 +269,9 @@ describe("connection tests", function () {
var end = helper.callFuncAfter(done, 2);
client.once('ready', function() {
end();
});
client.once('ready', end);
client.set('foo', 'bar', end);
});
}
it("connects correctly with args", function (done) {
client = redis.createClient.apply(redis.createClient, args);
@ -221,9 +279,7 @@ describe("connection tests", function () {
client.once("ready", function () {
client.removeListener("error", done);
client.get("recon 1", function (err, res) {
done(err);
});
client.get("recon 1", done);
});
});
@ -233,9 +289,7 @@ describe("connection tests", function () {
client.once("ready", function () {
client.removeListener("error", done);
client.get("recon 1", function (err, res) {
done(err);
});
client.get("recon 1", done);
});
});
@ -246,9 +300,7 @@ describe("connection tests", function () {
client.once("ready", function () {
client.removeListener("error", done);
client.get("recon 1", function (err, res) {
done(err);
});
client.get("recon 1", done);
});
});
@ -258,9 +310,7 @@ describe("connection tests", function () {
client.once("ready", function () {
client.removeListener("error", done);
client.get("recon 1", function (err, res) {
done(err);
});
client.get("recon 1", done);
});
});
@ -339,36 +389,21 @@ describe("connection tests", function () {
assert(create_stream_string === String(redis.RedisClient.prototype.create_stream));
});
it("throws on strange connection info", function () {
client = {
end: function() {}
};
try {
redis.createClient(true);
throw new Error('failed');
} catch (err) {
assert.equal(err.message, 'Unknown type of connection in createClient()');
}
});
it("throws on protocol other than redis in the redis url", function () {
client = {
end: function() {}
};
try {
redis.createClient(config.HOST[ip] + ':' + config.PORT);
throw new Error('failed');
} catch (err) {
assert.equal(err.message, 'Connection string must use the "redis:" protocol or begin with slashes //');
}
if (ip === 'IPv4') {
it('allows connecting with the redis url to the default host and port, select db 3 and warn about duplicate db option', function (done) {
client = redis.createClient('redis:///3?db=3');
assert.strictEqual(client.selected_db, '3');
client.on("ready", done);
});
if (ip === 'IPv4') {
it('allows connecting with the redis url and the default port', function (done) {
it('allows connecting with the redis url and the default port and auth provided even though it is not required', function (done) {
client = redis.createClient('redis://:porkchopsandwiches@' + config.HOST[ip] + '/');
client.on("ready", function () {
return done();
var end = helper.callFuncAfter(done, 2);
client.on('warning', function (msg) {
assert.strictEqual(msg, 'Warning: Redis server does not require a password, but a password was supplied.');
end();
});
client.on("ready", end);
});
it('allows connecting with the redis url as first parameter and the options as second parameter', function (done) {
@ -376,9 +411,7 @@ describe("connection tests", function () {
connect_timeout: 1000
});
assert.strictEqual(client.options.connect_timeout, 1000);
client.on('ready', function () {
done();
});
client.on('ready', done);
});
it('allows connecting with the redis url in the options object and works with protocols other than the redis protocol (e.g. http)', function (done) {
@ -389,9 +422,7 @@ describe("connection tests", function () {
assert.strictEqual(+client.selected_db, 3);
assert(!client.options.port);
assert.strictEqual(client.options.host, config.HOST[ip]);
client.on("ready", function () {
return done();
});
client.on("ready", done);
});
it('allows connecting with the redis url and no auth and options as second parameter', function (done) {
@ -400,18 +431,14 @@ describe("connection tests", function () {
};
client = redis.createClient('redis://' + config.HOST[ip] + ':' + config.PORT, options);
assert.strictEqual(Object.keys(options).length, 1);
client.on("ready", function () {
return done();
});
client.on("ready", done);
});
it('allows connecting with the redis url and no auth and options as third parameter', function (done) {
client = redis.createClient('redis://' + config.HOST[ip] + ':' + config.PORT, null, {
detect_buffers: false
});
client.on("ready", function () {
return done();
});
client.on("ready", done);
});
}

41
test/helper.js

@ -8,26 +8,11 @@ var StunnelProcess = require("./lib/stunnel-process");
var rp;
var stunnel_process;
function startRedis (conf, done) {
function startRedis (conf, done, port) {
RedisProcess.start(function (err, _rp) {
rp = _rp;
return done(err);
}, path.resolve(__dirname, conf));
}
function startStunnel(done) {
StunnelProcess.start(function (err, _stunnel_process) {
stunnel_process = _stunnel_process;
return done(err);
}, path.resolve(__dirname, './conf'));
}
function stopStunnel(done) {
if (stunnel_process) {
StunnelProcess.stop(stunnel_process, done);
} else {
done();
}
}, path.resolve(__dirname, conf), port);
}
// don't start redis every time we
@ -52,8 +37,19 @@ module.exports = {
rp.stop(done);
},
startRedis: startRedis,
stopStunnel: stopStunnel,
startStunnel: startStunnel,
stopStunnel: function (done) {
if (stunnel_process) {
StunnelProcess.stop(stunnel_process, done);
} else {
done();
}
},
startStunnel: function (done) {
StunnelProcess.start(function (err, _stunnel_process) {
stunnel_process = _stunnel_process;
return done(err);
}, path.resolve(__dirname, './conf'));
},
isNumber: function (expected, done) {
return function (err, results) {
assert.strictEqual(null, err, "expected " + expected + ", got error: " + err);
@ -171,9 +167,12 @@ module.exports = {
},
callFuncAfter: function (func, max) {
var i = 0;
return function () {
return function (err) {
if (err) {
throw err;
}
i++;
if (i === max) {
if (i >= max) {
func();
return true;
}

45
test/lib/redis-process.js

@ -5,30 +5,51 @@ var config = require('./config');
var fs = require('fs');
var path = require('path');
var spawn = require('win-spawn');
var spawnFailed = false;
var tcpPortUsed = require('tcp-port-used');
var bluebird = require('bluebird');
// wait for redis to be listening in
// all three modes (ipv4, ipv6, socket).
function waitForRedis (available, cb) {
function waitForRedis (available, cb, port) {
if (process.platform === 'win32') return cb();
var ipV4 = false;
var time = Date.now();
var running = false;
var socket = '/tmp/redis.sock';
if (port) {
// We have to distinguishe the redis sockets if we have more than a single redis instance running
socket = '/tmp/redis' + port + '.sock';
}
port = port || config.PORT;
var id = setInterval(function () {
tcpPortUsed.check(config.PORT, '127.0.0.1').then(function (_ipV4) {
ipV4 = _ipV4;
return tcpPortUsed.check(config.PORT, '::1');
}).then(function (ipV6) {
if (ipV6 === available && ipV4 === available && fs.existsSync('/tmp/redis.sock') === available) {
if (running) return;
running = true;
bluebird.join(
tcpPortUsed.check(port, '127.0.0.1'),
tcpPortUsed.check(port, '::1'),
function (ipV4, ipV6) {
if (ipV6 === available && ipV4 === available) {
if (fs.existsSync(socket) === available) {
clearInterval(id);
return cb();
}
// The same message applies for can't stop but we ignore that case
throw new Error('Port ' + port + ' is already in use. Tests can\'t start.\n');
}
if (Date.now() - time > 6000) {
throw new Error('Redis could not start on port ' + (port || config.PORT) + '\n');
}
running = false;
}).catch(function (err) {
console.error('\x1b[31m' + err.stack + '\x1b[0m\n');
process.exit(1);
});
}, 100);
}
module.exports = {
start: function (done, conf) {
start: function (done, conf, port) {
var spawnFailed = false;
// spawn redis with our testing configuration.
var confFile = conf || path.resolve(__dirname, '../conf/redis.conf');
var rp = spawn("redis-server", [confFile], {});
@ -53,15 +74,15 @@ module.exports = {
rp.once("exit", function (code) {
var error = null;
if (code !== null && code !== 0) {
error = Error('Redis shutdown failed with code ' + code);
error = new Error('Redis shutdown failed with code ' + code);
}
waitForRedis(false, function () {
return done(error);
});
}, port);
});
rp.kill("SIGTERM");
}
});
});
}, port);
}
};

7
test/lib/stunnel-process.js

@ -2,11 +2,16 @@
// helper to start and stop the stunnel process.
var spawn = require('child_process').spawn;
var EventEmitter = require('events').EventEmitter;
var EventEmitter = require('events');
var fs = require('fs');
var path = require('path');
var util = require('util');
// Newer Node.js versions > 0.10 return the EventEmitter right away and using .EventEmitter was deprecated
if (typeof EventEmitter !== 'function') {
EventEmitter = EventEmitter.EventEmitter;
}
function once(cb) {
var called = false;
return function() {

35
test/multi.spec.js

@ -70,6 +70,28 @@ describe("The 'multi' method", function () {
});
});
describe('pipeline limit', function () {
it('do not exceed maximum string size', function (done) {
this.timeout(25000); // Windows tests are horribly slow
// Triggers a RangeError: Invalid string length if not handled properly
client = redis.createClient();
var multi = client.multi();
var i = Math.pow(2, 28);
while (i > 0) {
i -= 10230;
multi.set('foo' + i, 'bar' + new Array(1024).join('1234567890'));
}
client.on('ready', function () {
multi.exec(function (err, res) {
assert.strictEqual(res.length, 26241);
});
client.flushdb(done);
});
});
});
helper.allTests(function(parser, ip, args) {
describe("using " + parser + " and " + ip, function () {
@ -98,7 +120,7 @@ describe("The 'multi' method", function () {
assert(err.message.match(/The connection has already been closed/));
done();
});
assert.strictEqual(notBuffering, true);
assert.strictEqual(notBuffering, false);
});
it("reports an error if promisified", function () {
@ -241,6 +263,7 @@ describe("The 'multi' method", function () {
multi1.set("m1", "123");
multi1.get('m1');
multi2.get('m2');
multi2.ping();
multi1.exec(end);
multi2.exec(function(err, res) {
@ -330,8 +353,8 @@ describe("The 'multi' method", function () {
arr4,
[["mset", "multifoo2", "multibar2", "multifoo3", "multibar3"], helper.isString('OK')],
["hmset", arr],
[["hmset", "multihmset2", "multibar2", "multifoo3", "multibar3", "test", helper.isString('OK')]],
["hmset", ["multihmset", "multibar", "multifoo", helper.isString('OK')]],
[["hmset", "multihmset2", "multibar2", "multifoo3", "multibar3", "test"], helper.isString('OK')],
["hmset", ["multihmset", "multibar", "multifoo"], helper.isString('OK')],
["hmset", arr3, helper.isString('OK')],
['hmset', now, {123456789: "abcdefghij", "some manner of key": "a type of value", "otherTypes": 555}],
['hmset', 'key2', {"0123456789": "abcdefghij", "some manner of key": "a type of value", "otherTypes": 999}, helper.isString('OK')],
@ -399,7 +422,7 @@ describe("The 'multi' method", function () {
it('allows multiple commands to work the same as normal to be performed using a chaining API', function (done) {
client.multi()
.mset(['some', '10', 'keys', '20'])
.incr(['some', helper.isNumber(11)])
.incr('some', helper.isNumber(11))
.incr(['keys'], helper.isNumber(21))
.mget('some', 'keys')
.exec(function (err, replies) {
@ -416,7 +439,7 @@ describe("The 'multi' method", function () {
it('allows multiple commands to work the same as normal to be performed using a chaining API promisified', function () {
return client.multi()
.mset(['some', '10', 'keys', '20'])
.incr(['some', helper.isNumber(11)])
.incr('some', helper.isNumber(11))
.incr(['keys'], helper.isNumber(21))
.mget('some', 'keys')
.execAsync()
@ -538,7 +561,7 @@ describe("The 'multi' method", function () {
client.get('foo', helper.isString('bar', done));
});
it("should not use a transaction with exec_atomic if only no command is used", function () {
it("should not use a transaction with exec_atomic if no command is used", function () {
var multi = client.multi();
var test = false;
multi.exec_batch = function () {

91
test/node_redis.spec.js

@ -384,9 +384,18 @@ describe("The node_redis client", function () {
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', done));
client.get('foo', helper.isString('bar', end));
});
client.set('foo', 'bar');
});
@ -423,6 +432,41 @@ describe("The node_redis client", function () {
});
});
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;
});
});
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, {
@ -550,6 +594,28 @@ describe("The node_redis client", function () {
});
});
describe('protocol error', function () {
it("should gracefully recover and only fail on the already send commands", function (done) {
client = redis.createClient.apply(redis.createClient, args);
client.on('error', function(err) {
assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte');
// 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, 'Protocol error, got "a" as reply type byte');
});
// Fail the set answer. Has no corresponding command obj and will therefor 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) {
@ -557,9 +623,17 @@ describe("The node_redis client", function () {
parser: parser,
no_ready_check: true
});
var end = helper.callFuncAfter(done, 2);
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();
@ -616,6 +690,9 @@ describe("The node_redis client", function () {
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);
});
@ -649,10 +726,6 @@ describe("The node_redis client", function () {
helper.killConnection(client);
});
client.on("reconnecting", function (params) {
assert.equal(client.command_queue.length, 15);
});
client.on('error', function(err) {
if (/uncertain state/.test(err.message)) {
assert.equal(client.command_queue.length, 0);
@ -674,7 +747,7 @@ describe("The node_redis client", function () {
enable_offline_queue: false
});
client.on('ready', function () {
client.stream.writable = false;
client.stream.destroy();
client.set('foo', 'bar', function (err, res) {
assert.strictEqual(err.message, "SET can't be processed. Stream not writeable.");
done();
@ -730,10 +803,6 @@ describe("The node_redis client", function () {
helper.killConnection(client);
});
client.on("reconnecting", function (params) {
assert.equal(client.command_queue.length, 15);
});
client.on('error', function(err) {
if (err.code === 'UNCERTAIN_STATE') {
assert.equal(client.command_queue.length, 0);

29
test/rename.spec.js

@ -5,6 +5,11 @@ var config = require("./lib/config");
var helper = require('./helper');
var redis = config.redis;
if (process.platform === 'win32') {
// TODO: Fix redis process spawn on windows
return;
}
describe("rename commands", function () {
before(function (done) {
helper.stopRedis(function () {
@ -18,6 +23,7 @@ describe("rename commands", function () {
var client = null;
beforeEach(function(done) {
if (helper.redisProcess().spawnFailed()) return done();
client = redis.createClient({
rename_commands: {
set: '807081f5afa96845a02816a28b7258c3',
@ -27,11 +33,12 @@ describe("rename commands", function () {
});
client.on('ready', function () {
done();
client.flushdb(done);
});
});
afterEach(function () {
if (helper.redisProcess().spawnFailed()) return;
client.end(true);
});
@ -109,10 +116,30 @@ describe("rename commands", function () {
});
});
it("should also work prefixed commands", function (done) {
if (helper.redisProcess().spawnFailed()) this.skip();
client.end(true);
client = redis.createClient({
rename_commands: {
set: '807081f5afa96845a02816a28b7258c3'
},
parser: parser,
prefix: 'baz'
});
client.set('foo', 'bar');
client.keys('*', function(err, reply) {
assert.strictEqual(reply[0], 'bazfoo');
assert.strictEqual(err, null);
done();
});
});
});
});
after(function (done) {
if (helper.redisProcess().spawnFailed()) return done();
helper.stopRedis(function () {
helper.startRedis('./conf/redis.conf', done);
});

9
test/return_buffers.spec.js

@ -18,17 +18,24 @@ describe("return_buffers", function () {
beforeEach(function (done) {
client = redis.createClient.apply(redis.createClient, args);
var i = 1;
if (args[2].detect_buffers) {
// Test if detect_buffer option was deactivated
assert.strictEqual(client.options.detect_buffers, false);
args[2].detect_buffers = false;
i++;
}
var end = helper.callFuncAfter(done, i);
client.on('warning', function (msg) {
assert.strictEqual(msg, 'WARNING: You activated return_buffers and detect_buffers at the same time. The return value is always going to be a buffer.');
end();
});
client.once("error", done);
client.once("connect", function () {
client.flushdb(function (err) {
client.hmset("hash key 2", "key 1", "val 1", "key 2", "val 2");
client.set("string key 1", "string value");
return done(err);
end(err);
});
});
});

174
test/tls.spec.js

@ -6,87 +6,55 @@ var fs = require('fs');
var helper = require('./helper');
var path = require('path');
var redis = config.redis;
var utils = require('../lib/utils');
var tls_options = {
servername: "redis.js.org",
rejectUnauthorized: false,
rejectUnauthorized: true,
ca: [ String(fs.readFileSync(path.resolve(__dirname, "./conf/redis.js.org.cert"))) ]
};
var tls_port = 6380;
if (process.platform === 'win32') {
return;
}
// Use skip instead of returning to indicate what tests really got skipped
var skip = false;
// Wait until stunnel4 is in the travis whitelist
// Check: https://github.com/travis-ci/apt-package-whitelist/issues/403
// If this is merged, remove the travis env checks
describe("TLS connection tests", function () {
before(function (done) {
if (process.env.TRAVIS === 'true') {
done();
return;
// Print the warning when the tests run instead of while starting mocha
if (process.platform === 'win32') {
skip = true;
console.warn('\nStunnel tests do not work on windows atm. If you think you can fix that, it would be warmly welcome.\n');
} else if (process.env.TRAVIS === 'true') {
skip = true;
console.warn('\nTravis does not support stunnel right now. Skipping tests.\nCheck: https://github.com/travis-ci/apt-package-whitelist/issues/403\n');
}
if (skip) return done();
helper.stopStunnel(function () {
helper.startStunnel(done);
});
});
after(function (done) {
if (process.env.TRAVIS === 'true') {
done();
return;
}
if (skip) return done();
helper.stopStunnel(done);
});
helper.allTests(function(parser, ip, args) {
describe("using " + parser + " and " + ip, function () {
var client;
afterEach(function () {
if (skip) return;
client.end(true);
});
describe("on lost connection", function () {
it("emit an error after max retry attempts and do not try to reconnect afterwards", function (done) {
if (process.env.TRAVIS === 'true') this.skip();
var max_attempts = 4;
var options = {
parser: parser,
max_attempts: max_attempts,
port: tls_port,
tls: tls_options
};
client = redis.createClient(options);
var calls = 0;
client.once('ready', function() {
helper.killConnection(client);
});
client.on("reconnecting", function (params) {
calls++;
});
client.on('error', function(err) {
if (/Redis connection in broken state: maximum connection attempts.*?exceeded./.test(err.message)) {
setTimeout(function () {
assert.strictEqual(calls, max_attempts - 1);
done();
}, 500);
}
});
});
it("emit an error after max retry timeout and do not try to reconnect afterwards", function (done) {
if (process.env.TRAVIS === 'true') this.skip();
if (skip) this.skip();
var connect_timeout = 500; // in ms
client = redis.createClient({
parser: parser,
connect_timeout: connect_timeout,
port: tls_port,
tls: tls_options
@ -106,116 +74,50 @@ describe("TLS connection tests", function () {
setTimeout(function () {
assert(time === connect_timeout);
done();
}, 500);
}, 100);
}
});
});
it("end connection while retry is still ongoing", function (done) {
if (process.env.TRAVIS === 'true') this.skip();
var connect_timeout = 1000; // in ms
client = redis.createClient({
parser: parser,
connect_timeout: connect_timeout,
port: tls_port,
tls: tls_options
});
client.once('ready', function() {
helper.killConnection(client);
});
client.on("reconnecting", function (params) {
client.end(true);
setTimeout(done, 100);
});
});
it("can not connect with wrong host / port in the options object", function (done) {
if (process.env.TRAVIS === 'true') this.skip();
var options = {
host: 'somewhere',
max_attempts: 1,
port: tls_port,
tls: tls_options
};
client = redis.createClient(options);
var end = helper.callFuncAfter(done, 2);
client.on('error', function (err) {
assert(/CONNECTION_BROKEN|ENOTFOUND|EAI_AGAIN/.test(err.code));
end();
});
});
});
describe("when not connected", function () {
it("connect with host and port provided in the options object", function (done) {
if (process.env.TRAVIS === 'true') this.skip();
if (skip) this.skip();
client = redis.createClient({
host: 'localhost',
parser: parser,
connect_timeout: 1000,
port: tls_port,
tls: tls_options
});
client.once('ready', function() {
done();
});
});
it("connects correctly with args", function (done) {
if (process.env.TRAVIS === 'true') this.skip();
var args_host = args[1];
var args_options = args[2] || {};
args_options.tls = tls_options;
client = redis.createClient(tls_port, args_host, args_options);
client.on("error", done);
client.once("ready", function () {
client.removeListener("error", done);
client.get("recon 1", function (err, res) {
done(err);
});
});
});
if (ip === 'IPv4') {
it('allows connecting with the redis url and no auth and options as second parameter', function (done) {
if (process.env.TRAVIS === 'true') this.skip();
var options = {
detect_buffers: false,
magic: Math.random(),
port: tls_port,
tls: tls_options
};
client = redis.createClient('redis://' + config.HOST[ip] + ':' + tls_port, options);
// verify connection is using TCP, not UNIX socket
assert.strictEqual(client.connection_options.host, config.HOST[ip]);
assert.strictEqual(client.connection_options.host, 'localhost');
assert.strictEqual(client.connection_options.port, tls_port);
assert(typeof client.stream.getCipher === 'function');
// verify passed options are in use
assert.strictEqual(client.options.magic, options.magic);
client.on("ready", function () {
return done();
});
});
assert(client.stream.encrypted);
it('allows connecting with the redis url and no auth and options as third parameter', function (done) {
if (process.env.TRAVIS === 'true') this.skip();
client = redis.createClient('redis://' + config.HOST[ip] + ':' + tls_port, null, {
detect_buffers: false,
tls: tls_options
client.set('foo', 'bar');
client.get('foo', helper.isString('bar', done));
});
client.on("ready", function () {
return done();
it('fails to connect because the cert is not correct', function (done) {
if (skip) this.skip();
var faulty_cert = utils.clone(tls_options);
faulty_cert.ca = [ String(fs.readFileSync(path.resolve(__dirname, "./conf/faulty.cert"))) ];
client = redis.createClient({
host: 'localhost',
connect_timeout: 1000,
port: tls_port,
tls: faulty_cert
});
client.on('error', function (err) {
assert.strictEqual(err.code, 'DEPTH_ZERO_SELF_SIGNED_CERT');
client.end(true);
});
}
client.set('foo', 'bar', function (err, res) {
done(res);
});
});
});
});

241
test/unify_options.spec.js

@ -0,0 +1,241 @@
'use strict';
var assert = require('assert');
var unifyOptions = require('../lib/createClient');
var intercept = require('intercept-stdout');
describe('createClient options', function () {
describe('port as first parameter', function () {
it('pass the options in the second parameter after a port', function () {
var options = unifyOptions(1234, {
option1: true,
option2: function () {}
});
assert.strictEqual(Object.keys(options).length, 4);
assert(options.option1);
assert.strictEqual(options.port, 1234);
assert.strictEqual(options.host, undefined);
assert.strictEqual(typeof options.option2, 'function');
});
it('pass the options in the third parameter after a port and host being set to null', function () {
var options = unifyOptions(1234, null, {
option1: true,
option2: function () {}
});
assert.strictEqual(Object.keys(options).length, 4);
assert(options.option1);
assert.strictEqual(options.port, 1234);
assert.strictEqual(options.host, undefined);
assert.strictEqual(typeof options.option2, 'function');
});
it('pass the options in the third parameter after a port and host being set to undefined', function () {
var options = unifyOptions(1234, undefined, {
option1: true,
option2: function () {}
});
assert.strictEqual(Object.keys(options).length, 4);
assert(options.option1);
assert.strictEqual(options.port, 1234);
assert.strictEqual(options.host, undefined);
assert.strictEqual(typeof options.option2, 'function');
});
it('pass the options in the third parameter after a port and host', function () {
var options = unifyOptions('1234', 'localhost', {
option1: true,
option2: function () {}
});
assert.strictEqual(Object.keys(options).length, 4);
assert(options.option1);
assert.strictEqual(options.port, '1234');
assert.strictEqual(options.host, 'localhost');
assert.strictEqual(typeof options.option2, 'function');
});
it('should throw with three parameters all set to a truthy value', function () {
try {
unifyOptions(1234, {}, {});
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'Unknown type of connection in createClient()');
}
});
});
describe('unix socket as first parameter', function () {
it('pass the options in the second parameter after a port', function () {
var options = unifyOptions('/tmp/redis.sock', {
option1: true,
option2: function () {},
option3: [1, 2, 3]
});
assert.strictEqual(Object.keys(options).length, 4);
assert(options.option1);
assert.strictEqual(options.path, '/tmp/redis.sock');
assert.strictEqual(typeof options.option2, 'function');
assert.strictEqual(options.option3.length, 3);
});
it('pass the options in the third parameter after a port and host being set to null', function () {
var options = unifyOptions('/tmp/redis.sock', null, {
option1: true,
option2: function () {}
});
assert.strictEqual(Object.keys(options).length, 3);
assert(options.option1);
assert.strictEqual(options.path, '/tmp/redis.sock');
assert.strictEqual(typeof options.option2, 'function');
});
});
describe('redis url as first parameter', function () {
it('empty redis url including options as second parameter', function () {
var options = unifyOptions('redis://', {
option: [1, 2, 3]
});
assert.strictEqual(Object.keys(options).length, 1);
assert.strictEqual(options.option.length, 3);
});
it('begin with two slashes including options as third parameter', function () {
var options = unifyOptions('//:abc@/3?port=123', {
option: [1, 2, 3]
});
assert.strictEqual(Object.keys(options).length, 4);
assert.strictEqual(options.option.length, 3);
assert.strictEqual(options.port, '123');
assert.strictEqual(options.db, '3');
assert.strictEqual(options.password, 'abc');
});
it('duplicated, identical query options including options obj', function () {
var text = '';
var unhookIntercept = intercept(function(data) {
text += data;
return '';
});
var options = unifyOptions('//:abc@localhost:123/3?db=3&port=123&password=abc', null, {
option: [1, 2, 3]
});
unhookIntercept();
assert.strictEqual(text,
'node_redis: WARNING: You passed the db option twice!\n' +
'node_redis: WARNING: You passed the port option twice!\n' +
'node_redis: WARNING: You passed the password option twice!\n'
);
assert.strictEqual(Object.keys(options).length, 5);
assert.strictEqual(options.option.length, 3);
assert.strictEqual(options.host, 'localhost');
assert.strictEqual(options.port, '123');
assert.strictEqual(options.db, '3');
assert.strictEqual(options.password, 'abc');
});
it('should throw on duplicated, non-identical query options', function () {
try {
unifyOptions('//:abc@localhost:1234/3?port=123&password=abc');
throw new Error('failed');
} catch (err) {
assert.equal(err.message, 'The port option is added twice and does not match');
}
});
it('should throw without protocol slashes', function () {
try {
unifyOptions('redis:abc@localhost:123/3?db=3&port=123&password=abc');
throw new Error('failed');
} catch (err) {
assert.equal(err.message, 'The redis url must begin with slashes "//" or contain slashes after the redis protocol');
}
});
it("warns on protocol other than redis in the redis url", function () {
var text = '';
var unhookIntercept = intercept(function (data) {
text += data;
return '';
});
var options = unifyOptions('http://abc');
unhookIntercept();
assert.strictEqual(Object.keys(options).length, 1);
assert.strictEqual(options.host, 'abc');
assert.strictEqual(text, 'node_redis: WARNING: You passed "http" as protocol instead of the "redis" protocol!\n');
});
});
describe('no parameters or set to null / undefined', function () {
it('no parameters', function () {
var options = unifyOptions();
assert.strictEqual(Object.keys(options).length, 1);
assert.strictEqual(options.host, undefined);
});
it('set to null', function () {
var options = unifyOptions(null, null);
assert.strictEqual(Object.keys(options).length, 1);
assert.strictEqual(options.host, null);
});
it('set to undefined', function () {
var options = unifyOptions(undefined, undefined);
assert.strictEqual(Object.keys(options).length, 1);
assert.strictEqual(options.host, undefined);
});
});
describe('only an options object is passed', function () {
it('with options', function () {
var options = unifyOptions({
option: true
});
assert.strictEqual(Object.keys(options).length, 2);
assert.strictEqual(options.host, undefined);
assert.strictEqual(options.option, true);
});
it('without options', function () {
var options = unifyOptions({});
assert.strictEqual(Object.keys(options).length, 1);
assert.strictEqual(options.host, undefined);
});
it('should throw with more parameters', function () {
try {
unifyOptions({
option: true
}, undefined);
throw new Error('failed');
} catch (err) {
assert.strictEqual(err.message, 'To many arguments passed to createClient. Please only pass the options object');
}
});
it('including url as option', function () {
var options = unifyOptions({
option: [1, 2, 3],
url: '//hm:abc@localhost:123/3'
});
assert.strictEqual(Object.keys(options).length, 6);
assert.strictEqual(options.option.length, 3);
assert.strictEqual(options.host, 'localhost');
assert.strictEqual(options.port, '123');
assert.strictEqual(options.db, '3');
assert.strictEqual(options.url, '//hm:abc@localhost:123/3');
assert.strictEqual(options.password, 'abc');
});
});
describe('faulty data', function () {
it("throws on strange connection info", function () {
try {
unifyOptions(true);
throw new Error('failed');
} catch (err) {
assert.equal(err.message, 'Unknown type of connection in createClient()');
}
});
});
});

154
test/utils.spec.js

@ -0,0 +1,154 @@
'use strict';
var assert = require('assert');
var Queue = require('double-ended-queue');
var utils = require('../lib/utils');
var intercept = require('intercept-stdout');
describe('utils.js', function () {
describe('clone', function () {
it('ignore the object prototype and clone a nested array / object', function () {
var obj = {
a: [null, 'foo', ['bar'], {
"I'm special": true
}],
number: 5,
fn: function noop () {}
};
var clone = utils.clone(obj);
assert.deepEqual(clone, obj);
assert.strictEqual(obj.fn, clone.fn);
assert(typeof clone.fn === 'function');
});
it('replace faulty values with an empty object as return value', function () {
var a = utils.clone();
var b = utils.clone(null);
assert.strictEqual(Object.keys(a).length, 0);
assert.strictEqual(Object.keys(b).length, 0);
});
it('throws on circular data', function () {
try {
var a = {};
a.b = a;
utils.clone(a);
throw new Error('failed');
} catch (e) {
assert(e.message !== 'failed');
}
});
});
describe('print helper', function () {
it('callback with reply', function () {
var text = '';
var unhookIntercept = intercept(function(data) {
text += data;
return '';
});
utils.print(null, 'abc');
unhookIntercept();
assert.strictEqual(text, 'Reply: abc\n');
});
it('callback with error', function () {
var text = '';
var unhookIntercept = intercept(function(data) {
text += data;
return '';
});
utils.print(new Error('Wonderful exception'));
unhookIntercept();
assert.strictEqual(text, 'Error: Wonderful exception\n');
});
});
describe('reply_in_order', function () {
var err_count = 0;
var res_count = 0;
var emitted = false;
var clientMock = {
emit: function () { emitted = true; },
offline_queue: new Queue(),
command_queue: new Queue()
};
var create_command_obj = function () {
return {
callback: function (err, res) {
if (err) err_count++;
else res_count++;
}
};
};
beforeEach(function () {
clientMock.offline_queue.clear();
clientMock.command_queue.clear();
err_count = 0;
res_count = 0;
emitted = false;
});
it('no elements in either queue. Reply in the next tick', function (done) {
var called = false;
utils.reply_in_order(clientMock, function () {
called = true;
done();
}, null, null);
assert(!called);
});
it('no elements in either queue. Reply in the next tick', function (done) {
assert(!emitted);
utils.reply_in_order(clientMock, null, new Error('tada'));
assert(!emitted);
setTimeout(function () {
assert(emitted);
done();
}, 1);
});
it('elements in the offline queue. Reply after the offline queue is empty and respect the command_obj callback', function (done) {
clientMock.offline_queue.push(create_command_obj(), create_command_obj());
utils.reply_in_order(clientMock, function () {
assert.strictEqual(clientMock.offline_queue.length, 0);
assert.strictEqual(res_count, 2);
done();
}, null, null);
while (clientMock.offline_queue.length) clientMock.offline_queue.shift().callback(null, 'foo');
});
it('elements in the offline queue. Reply after the offline queue is empty and respect the command_obj error emit', function (done) {
clientMock.command_queue.push({}, create_command_obj(), {});
utils.reply_in_order(clientMock, function () {
assert.strictEqual(clientMock.command_queue.length, 0);
assert(emitted);
assert.strictEqual(err_count, 1);
assert.strictEqual(res_count, 0);
done();
}, null, null);
while (clientMock.command_queue.length) {
var command_obj = clientMock.command_queue.shift();
if (command_obj.callback) {
command_obj.callback(new Error('tada'));
}
}
});
it('elements in the offline queue. Reply after the offline queue is empty and respect the command_obj', function (done) {
clientMock.command_queue.push(create_command_obj(), {});
utils.reply_in_order(clientMock, function () {
assert.strictEqual(clientMock.command_queue.length, 0);
assert(!emitted);
assert.strictEqual(res_count, 1);
done();
}, null, null);
while (clientMock.command_queue.length) {
clientMock.command_queue.shift().callback(null, 'bar');
}
});
});
});
Loading…
Cancel
Save