├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── examples ├── auto-slaveok.js ├── basic.js ├── pubsub-oneclient.js └── pubsub.js ├── index.js ├── lib ├── multi.js ├── node.js └── uuid.js ├── package.json ├── test-singlemode.js ├── test.js └── test ├── _common.js ├── failover.js ├── node_redis.js └── quit.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | - '0.10' 6 | services: redis-server 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./node_modules/.bin/mocha \ 3 | --reporter spec \ 4 | --timeout 60s \ 5 | --bail \ 6 | --require test/_common.js 7 | 8 | test-cluster: 9 | @NODE_ENV=test node test 10 | 11 | .PHONY: test test-cluster 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | haredis 2 | ------- 3 | 4 | High-availability redis in Node.js 5 | 6 | [![build status](https://secure.travis-ci.org/carlos8f/haredis.png)](http://travis-ci.org/carlos8f/haredis) 7 | 8 | (note: Travis-CI support for this project is a work in progress. If the build 9 | badge is red above, it's likely not really a problem with haredis, but rather a 10 | problem with Travis-CI running my tests. Tests pass locally for me!) 11 | 12 | Idea 13 | ==== 14 | 15 | **haredis** is a code wrapper around [node_redis](https://github.com/mranney/node_redis) 16 | which adds fault-taulerance to your application. 17 | 18 | Features: 19 | 20 | - Drop-in replacement for [node_redis](https://github.com/mranney/node_redis) 21 | - Easily build a cluster out of 3 or more (default-configured) redis servers 22 | - Auto-failover due to connection drops 23 | - Master conflict resolution (default your servers to master, and **haredis** 24 | will elect the freshest and issue the `SLAVEOF` commands) 25 | - Freshness judged by an opcounter (incremented on write) 26 | - Locking mechanism to prevent failover contention 27 | - Load-balancing for reads and pub/sub 28 | - One-client pub/sub 29 | - Gossip channel for quick failover 30 | 31 | Usage 32 | ===== 33 | 34 | Start up multiple redis daemons, with no special configuration necessary, and 35 | list them like so: 36 | 37 | ```javascript 38 | var redis = require('haredis') 39 | , nodes = ['1.2.3.1:6379', '1.2.3.2:6379', '1.2.3.3:6379'] 40 | , client = redis.createClient(nodes) 41 | ; 42 | ``` 43 | 44 | ...then use `client` as you would use [node_redis](https://github.com/mranney/node_redis). 45 | If the master node goes down, **haredis** will automatically determine which node 46 | to promote to master, and keep standby connections to the others. 47 | 48 | If multiple **haredis** clients are connected, a locking mechanism is implemented 49 | to prevent contention between failover attempts. 50 | 51 | To see this in action, 52 | 53 | - Set up 3 local redis daemons on ports 6380-82 54 | - 6380 should be `SLAVEOF NO ONE`. 6381 and 82 should be slaves to 6380. 55 | - Run `test/basic.js` or `test/pubsub.js` (try multiple to test contention) 56 | - Kill the process listening on 6380 (master). **haredis** will auto-failover to 57 | the node it detects is freshest, set that to master, and the others to slaves! 58 | - Bring up 6380, and it will be added as standby for failover. 59 | 60 | API differences 61 | =============== 62 | 63 | `createClient` 64 | -------------- 65 | 66 | In **haredis**, `createClient` works like this: 67 | 68 | ```javascript 69 | function createClient([host/port array], options) 70 | ``` 71 | 72 | The first argument can be an array of hosts (using default port), ports (using 73 | localhost), or colon-separated strings (i.e., `1.2.3.4:6379`). **haredis** will 74 | attempt to connect to all of these servers. 75 | 76 | `options` corresponds to the same options you would pass 77 | [node_redis](https://github.com/mranney/node_redis). **haredis** 78 | additionally supports: 79 | 80 | - `haredis_db_num` {Number} database number that **haredis** should store metadata 81 | in (such as an opcounter). Defaults to `15`. 82 | 83 | `auth` 84 | ------ 85 | 86 | `auth` works like this: 87 | 88 | ```javascript 89 | function auth({host/port to password object}, callback) 90 | ``` 91 | 92 | The first argument can be an object of hosts, ports mapped to passwords 93 | (i.e., `{'1.2.3.1:6379': 'pass1', '1.2.3.3:6379': 'pass2'}`), or just the password 94 | string. 95 | 96 | Load-balancing 97 | ============== 98 | 99 | **haredis** can optionally load-balance read operations to random slaves. Pub/sub 100 | subscriptions will automatically try to use a slave. For normal read-only 101 | commands, you can choose to query a random slave by using the `slaveOk()` method: 102 | 103 | ```javascript 104 | client.slaveOk().GET('foo', function(err, reply) { ... 105 | ``` 106 | 107 | `slaveOk()` will only affect the current command. 108 | 109 | To load-balance all reads, you can set `options.auto_slaveok = true` in 110 | `createClient()`. Be advised that this can case problems due to replication delay! 111 | 112 | To force a read to go to master when using `auto_slaveok`, use `slaveOk(false)` 113 | before the command: 114 | 115 | ```javascript 116 | client.slaveOk(false).GET('foo', function(err, reply) { ... 117 | ``` 118 | 119 | One-client pub/sub 120 | ================== 121 | 122 | In redis, pub/sub is a "mode" which excludes the use of regular commands while 123 | subscriptions are active. Normally you need to make separate client objects to 124 | use `publish` on one and `subscribe` on the other. 125 | 126 | **haredis** adds the nice ability to use pub/sub simultaneously with regular 127 | commands. This is because it keeps internal redis clients in pub/sub mode for 128 | internal "gossip", but also makes it available for users. Of course this is 129 | optional, and you can always maintain a separate `haredis` client for subscribes 130 | if you wish. 131 | 132 | Advice 133 | ====== 134 | 135 | For proper failover, a majority of the nodes need to be still online. This means 136 | that the minimum number of nodes should be 3. Under the minimum setup, you can 137 | lose up to 1 node. If only 1/3 are up, commands will be queued indefinitely until 138 | another node comes up. 139 | 140 | Debugging/verbose logging 141 | ========================= 142 | 143 | To see what's under the hood, try setting `redis.debug_mode = true`, and you can 144 | see the failover process in detail: 145 | 146 | ``` 147 | [19:27:58](#1) warning: MASTER is down! (127.0.0.1:6380) 148 | [19:27:58](#1) info: reorientating (node down) in 2000ms 149 | Redis connection gone from end event. 150 | [19:28:00](#1) info: orientating (node down, 2/3 nodes up) ... 151 | [19:28:00](#1) warning: invalid master count: 0 152 | [19:28:00](#1) info: attempting failover! 153 | [19:28:00](#1) info: my failover id: gP0SCM1B 154 | [19:28:00](#1) info: lock was a success! 155 | [19:28:00](#1) info: 127.0.0.1:6381 had highest opcounter (1441) of 2 nodes. congrats! 156 | [19:28:00](#1) info: making 127.0.0.1:6382 into a slave... 157 | [19:28:00](#1) info: 127.0.0.1:6382 is slave 158 | [19:28:00](#1) info: publishing gossip:master for 127.0.0.1:6381 159 | [19:28:00](#1) info: renegotating subSlave away from master 160 | [19:28:00](#1) info: subSlave is now 127.0.0.1:6382 161 | [19:28:00](#1) info: ready, using 127.0.0.1:6381 as master 162 | ``` 163 | 164 | To get info on which commands are executed on which servers, try setting 165 | `redis.command_logging = true`. 166 | 167 | Running tests 168 | ============= 169 | 170 | **haredis** includes the test suite from [node_redis](https://github.com/mranney/node_redis) 171 | which can be run in single or clustered mode. 172 | 173 | If you have redis daemons running locally on ports 6380, 6381 and 8382, you can 174 | run the clustered test with: 175 | 176 | ```bash 177 | $ make test-cluster 178 | ``` 179 | 180 | Or in single-mode with a redis server on port 6379: 181 | 182 | ```bash 183 | $ make test 184 | ``` 185 | 186 | LICENSE - "MIT License" 187 | ======================= 188 | 189 | - Copyright (c) 2012 Carlos Rodriguez, http://s8f.org/ 190 | - Copyright (c) 2012 Terra Eclipse, Inc., http://www.terraeclipse.com/ 191 | 192 | Permission is hereby granted, free of charge, to any person 193 | obtaining a copy of this software and associated documentation 194 | files (the "Software"), to deal in the Software without 195 | restriction, including without limitation the rights to use, 196 | copy, modify, merge, publish, distribute, sublicense, and/or sell 197 | copies of the Software, and to permit persons to whom the 198 | Software is furnished to do so, subject to the following 199 | conditions: 200 | 201 | The above copyright notice and this permission notice shall be 202 | included in all copies or substantial portions of the Software. 203 | 204 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 205 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 206 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 207 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 208 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 209 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 210 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 211 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/auto-slaveok.js: -------------------------------------------------------------------------------- 1 | var redis = require('../') 2 | , uuid = require('../lib/uuid') 3 | ; 4 | 5 | redis.debug_mode = true; 6 | var client = redis.createClient([6380, 6381, 6382], {auto_slaveok: true}); 7 | 8 | var cmd_per_sec = 0; 9 | var err_per_sec = 0; 10 | 11 | setInterval(function() { 12 | var id = uuid(); 13 | client.SET('test', id, function(err, reply) { 14 | if (err) { 15 | err_per_sec++; 16 | return console.error(err); 17 | } 18 | cmd_per_sec++; 19 | }); 20 | client.GET('test', function (err, val) { 21 | if (err) { 22 | err_per_sec++; 23 | return console.error(err); 24 | } 25 | cmd_per_sec++; 26 | }); 27 | }, 20); 28 | 29 | setInterval(function() { 30 | console.log(cmd_per_sec + ' commands executed'); 31 | cmd_per_sec = 0; 32 | if (err_per_sec) { 33 | console.log(err_per_sec + ' errors'); 34 | err_per_sec = 0; 35 | } 36 | }, 1000); 37 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | var redis = require('../') 2 | , uuid = require('../lib/uuid') 3 | ; 4 | 5 | redis.debug_mode = true; 6 | var client = redis.createClient([6380, 6381, 6382]); 7 | 8 | var cmd_per_sec = 0; 9 | var err_per_sec = 0; 10 | 11 | setInterval(function() { 12 | var id = uuid(); 13 | client.SET('test', id, function(err, reply) { 14 | if (err) { 15 | err_per_sec++; 16 | return console.error(err); 17 | } 18 | cmd_per_sec++; 19 | }); 20 | }, 20); 21 | 22 | setInterval(function() { 23 | console.log(cmd_per_sec + ' commands executed'); 24 | cmd_per_sec = 0; 25 | if (err_per_sec) { 26 | console.log(err_per_sec + ' errors'); 27 | err_per_sec = 0; 28 | } 29 | }, 1000); -------------------------------------------------------------------------------- /examples/pubsub-oneclient.js: -------------------------------------------------------------------------------- 1 | var redis = require('../') 2 | , uuid = require('../lib/uuid') 3 | ; 4 | 5 | redis.debug_mode = true; 6 | var nodes = [6380, 6381, 6382]; 7 | var client = redis.createClient(nodes); 8 | 9 | client.on('subscribe', function(channel, count) { 10 | console.log('subscribed to ' + channel + ' on ' + client.subSlave + ' (' + count + ' subs)'); 11 | }); 12 | client.subscribe('mychannel', function(err, reply) { 13 | if (err) { 14 | return console.error(err); 15 | } 16 | }); 17 | 18 | var msg_per_sec = 0, pub_per_sec = 0, err_per_sec = 0; 19 | client.on('message', function(channel, message) { 20 | msg_per_sec++; 21 | }); 22 | 23 | setInterval(function() { 24 | var id = uuid(); 25 | client.publish('mychannel', id, function(err, reply) { 26 | if (err) { 27 | err_per_sec++; 28 | return console.error(err); 29 | } 30 | pub_per_sec++; 31 | }); 32 | }, 20); 33 | 34 | setInterval(function() { 35 | console.log(msg_per_sec + ' messages received'); 36 | msg_per_sec = 0; 37 | console.log(pub_per_sec + ' publishes'); 38 | pub_per_sec = 0; 39 | if (err_per_sec) { 40 | console.log(err_per_sec + ' errors'); 41 | err_per_sec = 0; 42 | } 43 | }, 1000); -------------------------------------------------------------------------------- /examples/pubsub.js: -------------------------------------------------------------------------------- 1 | var redis = require('../') 2 | , uuid = require('../lib/uuid') 3 | ; 4 | 5 | redis.debug_mode = true; 6 | var nodes = [6380, 6381, 6382]; 7 | var client = redis.createClient(nodes); 8 | var subClient = redis.createClient(nodes); 9 | 10 | subClient.on('subscribe', function(channel, count) { 11 | console.log('subscribed to ' + channel + ' on ' + subClient.subSlave + ' (' + count + ' subs)'); 12 | }); 13 | subClient.subscribe('mychannel', function(err, reply) { 14 | if (err) { 15 | return console.error(err); 16 | } 17 | }); 18 | 19 | var msg_per_sec = 0, pub_per_sec = 0, err_per_sec = 0; 20 | subClient.on('message', function(channel, message) { 21 | msg_per_sec++; 22 | }); 23 | 24 | setInterval(function() { 25 | var id = uuid(); 26 | client.publish('mychannel', id, function(err, reply) { 27 | if (err) { 28 | err_per_sec++; 29 | return console.error(err); 30 | } 31 | pub_per_sec++; 32 | }); 33 | }, 20); 34 | 35 | setInterval(function() { 36 | console.log(msg_per_sec + ' messages received'); 37 | msg_per_sec = 0; 38 | console.log(pub_per_sec + ' publishes'); 39 | pub_per_sec = 0; 40 | if (err_per_sec) { 41 | console.log(err_per_sec + ' errors'); 42 | err_per_sec = 0; 43 | } 44 | }, 1000); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis') 2 | , util = require('util') 3 | , EventEmitter = require('events').EventEmitter 4 | , Node = require('./lib/node') 5 | , HAMulti = require('./lib/multi') 6 | , connection_id = 0 7 | , commands = require('redis/lib/commands') 8 | , async = require('async') 9 | , uuid = require('./lib/uuid') 10 | ; 11 | 12 | require('pkginfo')(module); 13 | 14 | var log_level = {}; 15 | log_level.debug = 0x01; 16 | log_level.error = 0x02; 17 | log_level.warning = 0x04; 18 | log_level.info = 0x08; 19 | 20 | var defaults = { 21 | port: 6379, 22 | host: '127.0.0.1', 23 | lock_time: 1000, 24 | reorientate_wait: 2000, 25 | haredis_db_num: 15, 26 | socket_nodelay: true, 27 | log_level: log_level.error | log_level.warning | log_level.info, 28 | opcounterDiviser: 100 29 | }; 30 | 31 | function createClient(nodes, options, etc) { 32 | return new RedisHAClient(nodes, options, etc); 33 | } 34 | exports.createClient = createClient; 35 | exports.RedisClient = RedisHAClient; 36 | exports.debug_mode = false; 37 | exports.print = redis.print; 38 | exports.log_level = log_level; 39 | 40 | function RedisHAClient(nodeList, options) { 41 | var self = this; 42 | this.connection_id = ++connection_id; 43 | if (typeof arguments[0] == 'undefined' && typeof arguments[1] == 'undefined') { 44 | options = {single_mode: true}; 45 | nodeList = [defaults.port]; 46 | self.applyDefaults(options); 47 | self.debug('no arguments passed, starting client in local single server mode'); 48 | } 49 | else if (!util.isArray(nodeList)) { 50 | var port = nodeList ? nodeList : defaults.port; 51 | var host = options ? options : defaults.host; 52 | nodeList = [host + ':' + port]; 53 | options = typeof arguments[2] != 'undefined' ? arguments[2] : {}; 54 | options.single_mode = true; 55 | self.applyDefaults(options); 56 | self.debug('a port/host combo was passed, starting client in single server mode'); 57 | } 58 | else { 59 | self.applyDefaults(options); 60 | } 61 | EventEmitter.call(this); 62 | this.orientating = false; 63 | this.haredis_version = exports.version; 64 | 65 | this.nodes = []; 66 | this.queue = []; 67 | this.subscriptions = {}; 68 | this.psubscriptions = {}; 69 | this.selected_db = 0; 70 | this.connected = false; 71 | this.ready = false; 72 | this._slaveOk = false; 73 | this.opcounter = 0; 74 | this.on('connect', function() { 75 | // Mirror some stuff from master client, to better simulate node_redis. 76 | this.host = this.master.host; 77 | this.port = this.master.port; 78 | this.stream = this.master.client.stream; 79 | this.reply_parser = this.master.client.reply_parser; 80 | this.send_command = this.master.client.send_command.bind(this.master.client); 81 | this.connected = true; 82 | if (this.auth_callback) { 83 | // Note: response is simulated :) Auth would've happened by now on 84 | // all nodes. 85 | this.auth_callback(null, 'OK'); 86 | } 87 | }); 88 | this.parseNodeList(nodeList, this.options); 89 | } 90 | util.inherits(RedisHAClient, EventEmitter); 91 | 92 | RedisHAClient.prototype.applyDefaults = function(options) { 93 | var self = this; 94 | options = options || {}; 95 | this.options = {}; 96 | Object.keys(options).forEach(function(k) { 97 | self.options[k] = options[k]; 98 | }); 99 | Object.keys(defaults).forEach(function(k) { 100 | if (typeof self.options[k] == 'undefined') { 101 | self.options[k] = defaults[k]; 102 | } 103 | }); 104 | }; 105 | 106 | RedisHAClient.prototype.debug = function(message, label) { 107 | if (this.options.log_level & log_level.debug || exports.debug_mode) { 108 | arguments[0] = this.logFormat('debug', message); 109 | console.log.apply(null, arguments); 110 | } 111 | }; 112 | RedisHAClient.prototype.log = function(message, label) { 113 | if (this.options.log_level & log_level.info || exports.debug_mode) { 114 | arguments[0] = this.logFormat('info', message); 115 | console.log.apply(null, arguments); 116 | } 117 | }; 118 | RedisHAClient.prototype.warn = function(message, label) { 119 | if (this.options.log_level & log_level.warning || exports.debug_mode) { 120 | arguments[0] = this.logFormat('warning', message); 121 | console.log.apply(null, arguments); 122 | } 123 | }; 124 | RedisHAClient.prototype.error = function(message, label) { 125 | if (this.options.log_level & log_level.error || exports.debug_mode) { 126 | arguments[0] = this.logFormat('ERROR', message); 127 | console.error.apply(null, arguments); 128 | } 129 | }; 130 | RedisHAClient.prototype.logFormat = function(type, message) { 131 | return util.format('[%s](haredis#%d) %s: %s', new Date().toTimeString().split(' ')[0], this.connection_id, type, message); 132 | }; 133 | 134 | RedisHAClient.prototype.onReady = function() { 135 | var self = this; 136 | this.designateSubSlave(function(err) { 137 | if (err) { 138 | self.ready = false; 139 | return self.reorientate('unable to designate subslave'); 140 | } 141 | self.ready = true; 142 | self.orientating = false; 143 | self.evaluateNew = false; 144 | function onDrain() { 145 | self.emit('drain'); 146 | } 147 | function onIdle() { 148 | if (self.queue.length === 0) { 149 | self.emit('idle'); 150 | } 151 | } 152 | self.nodes.forEach(function(node) { 153 | if (node.client) { 154 | node.client.removeListener('drain', onDrain); 155 | node.client.removeListener('idle', onIdle); 156 | } 157 | }); 158 | if (!self.server_info) { 159 | self.debug('ready, using ' + self.master + ' as master'); 160 | } 161 | else { 162 | self.warn('orientate complete, using ' + self.master + ' as master'); 163 | } 164 | self.master.client.on('drain', onDrain); 165 | self.master.client.on('idle', onIdle); 166 | self.server_info = self.master.info; 167 | 168 | self.emit('connect'); 169 | self.emit('ready'); 170 | self.drainQueue(); 171 | }); 172 | }; 173 | 174 | RedisHAClient.prototype.slaveOk = RedisHAClient.prototype.slaveOK = function(command) { 175 | if (typeof command === 'boolean') { 176 | this._slaveOk = command; 177 | } 178 | else if (command) { 179 | return (this._slaveOk || this.options.auto_slaveok) && this.isRead(command); 180 | } 181 | else { 182 | this._slaveOk = true; 183 | } 184 | return this; 185 | }; 186 | 187 | commands.forEach(function(k) { 188 | commands.push(k.toUpperCase()); 189 | }); 190 | 191 | commands.forEach(function(k) { 192 | RedisHAClient.prototype[k] = function() { 193 | var args = Array.prototype.slice.call(arguments); 194 | var self = this; 195 | k = k.toLowerCase(); 196 | if (k == 'multi') { 197 | return new HAMulti(this, args[0]); 198 | } 199 | if (!this.ready) { 200 | // @todo: create a custom multi() method which can return a chainable thing 201 | // instead here. 202 | this.queue.push([k, args]); 203 | return; 204 | } 205 | 206 | var skipOpcounter = false; 207 | switch (k) { 208 | case 'subscribe': 209 | case 'unsubscribe': 210 | case 'psubscribe': 211 | case 'punsubscribe': 212 | // Maintain a hash of subscriptions, so we can move subscriptions around to 213 | // different slaves. 214 | var type = k[0] == 'p' ? 'psubscriptions' : 'subscriptions'; 215 | var unsub = k.indexOf('unsub') !== -1; 216 | for (var i in args) { 217 | if (typeof args[i] == 'string') { 218 | if (unsub) { 219 | delete this[type][args[i]]; 220 | } 221 | else { 222 | this[type][args[i]] = true; 223 | } 224 | } 225 | } 226 | self.debug(k + ' on ' + this.subSlave); 227 | return callCommand(this.subSlave.subClient, k, args, true); 228 | case 'select': 229 | // Need to execute on all nodes. 230 | // Execute on master first in case there is a callback. 231 | this.selected_db = parseInt(args[0]); 232 | callCommand(this.master.client, k, args, true); 233 | // Execute on slaves without a callback. 234 | this.slaves.forEach(function(node) { 235 | callCommand(node.client, k, [args[0]], true); 236 | }); 237 | return; 238 | case 'quit': 239 | self.debug('got quit'); 240 | var tasks = []; 241 | this.up.forEach(function(node) { 242 | tasks.push(function(cb) { 243 | node.quit(cb); 244 | }); 245 | }); 246 | async.parallel(tasks, function(err, replies) { 247 | self.emit('end'); 248 | self.debug('done quitting'); 249 | if (typeof args[0] == 'function') { 250 | args[0](err); 251 | } 252 | }); 253 | return; 254 | case 'monitor': 255 | case 'info': 256 | case 'config': 257 | case 'publish': 258 | skipOpcounter = true; 259 | break; 260 | } 261 | 262 | var client, node; 263 | if (this.slaveOk(k)) { 264 | if (node = this.randomSlave()) { 265 | self.debug(k + ' on ' + node); 266 | client = node.client; 267 | } 268 | } 269 | 270 | callCommand(client, k, args, skipOpcounter); 271 | 272 | function callCommand(client, command, args, skipOpcounter) { 273 | self._slaveOk = false; 274 | if (!client) { 275 | self.debug(command + ' on ' + self.master + ' (master default)'); 276 | client = self.master.client; 277 | } 278 | client[command].apply(client, args); 279 | // Increment opcounter if necessary. 280 | if (!self.isRead(command, args) && !skipOpcounter) { 281 | self.incrOpcounter(function(err) { 282 | if (err) { 283 | // Will trigger failover! 284 | return self.master.emit('err', err); 285 | } 286 | }); 287 | } 288 | } 289 | }; 290 | }); 291 | 292 | RedisHAClient.prototype.incrOpcounter = function(count, done) { 293 | if (typeof count == 'function') { 294 | done = count; 295 | count = 1; 296 | } 297 | if (this.options.single_mode) { 298 | done(); 299 | return; 300 | } 301 | var master = this.master; 302 | // For write operations, increment an op counter, to judge freshness of slaves. 303 | if (!master.opcounterClient) { 304 | master.opcounterClient = redis.createClient(master.port, master.host, this.options); 305 | if (master.auth_pass) { 306 | master.opcounterClient.auth(master.auth_pass); 307 | } 308 | master.clients.push(master.opcounterClient); 309 | if (this.options.haredis_db_num) { 310 | // Make redis connect to a special db (upon ready) for the opcounter. 311 | master.opcounterClient.selected_db = this.options.haredis_db_num; 312 | } 313 | master.opcounterClient.on('error', function(err) { 314 | master.emit('error', err); 315 | }); 316 | } 317 | if (this.opcounter++ % this.options.opcounterDiviser === 0) { 318 | master.opcounterClient.INCRBY('haredis:opcounter', count, done); 319 | } 320 | else { 321 | done(); 322 | } 323 | }; 324 | 325 | // Stash auth for connect and reconnect. Send immediately if already connected. 326 | RedisHAClient.prototype.auth = RedisHAClient.prototype.AUTH = function () { 327 | var args = Array.prototype.slice.call(arguments); 328 | var self = this; 329 | this.auth_callback = args[1]; 330 | var authList = args[0]; 331 | if (typeof authList == 'object'){ 332 | this.nodes.forEach(function(node) { 333 | var auth_pass = authList[node.host + ':' + node.port]; 334 | if (auth_pass) { 335 | node.auth_pass = auth_pass; 336 | } 337 | }); 338 | } else { 339 | this.nodes.forEach(function(node) { 340 | node.auth_pass = authList; 341 | }); 342 | } 343 | }; 344 | 345 | RedisHAClient.prototype.isRead = function(command, args) { 346 | switch (command.toLowerCase()) { 347 | case 'bitcount': 348 | case 'get': 349 | case 'getbit': 350 | case 'getrange': 351 | case 'hget': 352 | case 'hgetall': 353 | case 'hkeys': 354 | case 'hlen': 355 | case 'hmget': 356 | case 'hvals': 357 | case 'keys': 358 | case 'lindex': 359 | case 'llen': 360 | case 'lrange': 361 | case 'mget': 362 | case 'pttl': 363 | case 'scard': 364 | case 'sinter': 365 | case 'sismember': 366 | case 'smembers': 367 | case 'srandmember': 368 | case 'strlen': 369 | case 'sunion': 370 | case 'ttl': 371 | case 'type': 372 | case 'zcard': 373 | case 'zrange': 374 | case 'zrangebyscore': 375 | case 'zrank': 376 | case 'zrevrange': 377 | case 'zrevrangebyscore': 378 | case 'zrevrank': 379 | case 'zscore': 380 | return true; 381 | case 'sort': 382 | // @todo: parse to see if "store" is used 383 | return false; 384 | default: return false; 385 | } 386 | }; 387 | 388 | RedisHAClient.prototype.designateSubSlave = function(callback) { 389 | var self = this, tasks = []; 390 | if (this.subSlave) { 391 | if (this.subSlave.status == 'up') { 392 | if (!this.isMaster(this.subSlave)) { 393 | self.debug('still using ' + this.subSlave + ' as subSlave'); 394 | } 395 | else if (this.slaves.length > 0) { 396 | self.log('renegotating subSlave away from master'); 397 | var oldSubSlave = this.subSlave; 398 | Object.keys(self.subscriptions).forEach(function(channel) { 399 | tasks.push(function(cb) { 400 | self.log('unsubscribing ' + channel + ' on ' + oldSubSlave); 401 | oldSubSlave.subClient.unsubscribe(channel, cb); 402 | }); 403 | }); 404 | Object.keys(self.psubscriptions).forEach(function(pattern) { 405 | tasks.push(function(cb) { 406 | self.log('punsubscribing ' + pattern + ' on ' + oldSubSlave); 407 | oldSubSlave.subClient.punsubscribe(pattern, cb); 408 | }); 409 | }); 410 | this.subSlave = this.randomSlave(); 411 | self.log('subSlave is now ' + this.subSlave); 412 | } 413 | } 414 | else { 415 | this.subSlave = this.randomSlave(); 416 | if (this.subSlave) { 417 | self.warn('subSlave went down, renegotiated to ' + this.subSlave); 418 | } 419 | } 420 | } 421 | else if (this.slaves.length) { 422 | this.subSlave = this.randomSlave(); 423 | self.debug('designated ' + this.subSlave + ' as subSlave'); 424 | } 425 | if (!this.subSlave) { 426 | this.subSlave = this.master; 427 | self.debug('defaulting to master as subSlave'); 428 | } 429 | 430 | Object.keys(this.subscriptions).forEach(function(channel) { 431 | tasks.push(function(cb) { 432 | self.debug('subscribing ' + channel + ' on ' + self.subSlave); 433 | self.subSlave.subClient.subscribe(channel, cb); 434 | }); 435 | }); 436 | Object.keys(this.psubscriptions).forEach(function(pattern) { 437 | tasks.push(function(cb) { 438 | self.debug('psubscribing ' + pattern + ' on ' + self.subSlave); 439 | self.subSlave.subClient.psubscribe(pattern, cb); 440 | }); 441 | }); 442 | 443 | async.parallel(tasks, callback); 444 | }; 445 | 446 | RedisHAClient.prototype.drainQueue = function() { 447 | if (this.ready && this.queue.length) { 448 | // Call the next command in the queue 449 | var item = this.queue.shift(); 450 | this[item[0]].apply(this, item[1]); 451 | 452 | // Wait till nextTick to do next command 453 | var self = this; 454 | process.nextTick(function() { 455 | self.drainQueue(); 456 | }); 457 | } 458 | }; 459 | 460 | RedisHAClient.prototype.parseNodeList = function(nodeList, options) { 461 | var self = this; 462 | nodeList.forEach(function(n) { 463 | if (typeof n == 'object') { 464 | spec = n; 465 | } 466 | else { 467 | if (typeof n == 'number') { 468 | n = n + ""; 469 | } 470 | var parts = n.split(':'); 471 | var spec = {}; 472 | if (/^\d+$/.test(parts[0])) { 473 | spec.port = parseInt(parts[0]); 474 | spec.host = defaults.host; 475 | } 476 | else if (parts.length == 2) { 477 | spec.host = parts[0]; 478 | spec.port = parseInt(parts[1]); 479 | } 480 | else { 481 | spec.host = parts[0]; 482 | spec.port = defaults.port; 483 | } 484 | } 485 | var node = new Node(spec, options); 486 | node.on('up', function() { 487 | self.debug(this + ' is up'); 488 | if (self.responded.length == nodeList.length) { 489 | if (self.ready) { 490 | self.evaluateNew = true; 491 | self.orientate('evaluating ' + this); 492 | } 493 | else { 494 | self.orientate('looking for master'); 495 | } 496 | } 497 | }); 498 | node.on('reconnecting', function() { 499 | if (self.isMaster(this)) { 500 | self.warn('MASTER connection dropped, reconnecting...'); 501 | } 502 | else { 503 | self.warn(this + ' connection dropped, reconnecting...'); 504 | } 505 | self.connected = false; 506 | self.ready = false; 507 | // @todo: pass some real attempts/timers here 508 | self.emit('reconnecting', {}); 509 | }); 510 | node.on('down', function() { 511 | if (self.isMaster(this)) { 512 | self.error('MASTER is down! (' + this + ')'); 513 | } 514 | else { 515 | self.error(this + ' is down!'); 516 | } 517 | self.connected = false; 518 | self.ready = false; 519 | // @todo: pass some real attempts/timers here 520 | self.emit('reconnecting', {}); 521 | self.reorientate('node down'); 522 | }); 523 | node.on('error', function(err) { 524 | if (self.listeners('error').length) { 525 | self.emit('error', err); 526 | } 527 | else { 528 | self.warn(err); 529 | } 530 | }); 531 | node.on('subscribe', function(channel, count) { 532 | if (channel != 'haredis:gossip:master') { 533 | self.emit('subscribe', channel, count); 534 | } 535 | }); 536 | node.on('unsubscribe', function(channel, count) { 537 | self.emit('unsubscribe', channel, count); 538 | }); 539 | node.on('message', function(channel, message) { 540 | if (channel == 'haredis:gossip:master') { 541 | self.warn('gossip said ' + message + ' was promoted!'); 542 | if (!self.orientating && (!self.master || self.master.toString() != message)) { 543 | var node = self.nodeFromKey(message); 544 | if (!node) { 545 | return self.warn("can't find gossiped master!"); 546 | } 547 | if (node.status == 'up') { 548 | self.log('switching master...'); 549 | self.master = node; 550 | self.master.role = 'master'; 551 | self.onReady(); 552 | } 553 | else if (node.client) { 554 | // Hasten reconnect 555 | if (node.client.retry_timer) { 556 | clearInterval(node.client.retry_timer); 557 | } 558 | self.log('hastening reconnect...'); 559 | node.client.stream.connect(node.port, node.host); 560 | } 561 | } 562 | } 563 | else { 564 | self.emit('message', channel, message); 565 | } 566 | }); 567 | node.on('monitor', function(time, args) { 568 | self.emit('monitor', time, args); 569 | }); 570 | self.nodes.push(node); 571 | }); 572 | }; 573 | 574 | RedisHAClient.prototype.orientate = function(why) { 575 | var self = this; 576 | if ((!this.evaluateNew && this.ready) || this.orientating) { 577 | return; 578 | } 579 | self.debug('orientating (' + why + ', ' + this.up.length + '/' + this.nodes.length + ' nodes up) ...'); 580 | this.orientating = true; 581 | if (this.retryInterval) { 582 | clearInterval(this.retryInterval); 583 | } 584 | var tasks = []; 585 | this.ready = false; 586 | if ((this.up.length / this.down.length) <= 0.5) { 587 | self.warn('refusing to orientate without a majority up!'); 588 | return this.reorientate(why); 589 | } 590 | this.up.forEach(function(node) { 591 | tasks.push(function(cb) { 592 | node.client.INFO(function(err, reply) { 593 | if (err) { 594 | node.client.emit('error', err); 595 | // Purposely don't pass err to cb so parallel() can continue 596 | return cb(null, node); 597 | } 598 | var info = node.info = Node.parseInfo(reply); 599 | node.role = info.role; 600 | if (info.loading && info.loading !== '0') { 601 | err = new Error(node + ' still loading'); 602 | return cb(err); 603 | } 604 | else if (info.master_host) { 605 | // Resolve the host to prevent false duplication 606 | Node.resolveHost(info.master_host, function(err, host) { 607 | if (err) { 608 | node.client.emit('error', err); 609 | // Purposely don't pass err so parallel() can continue 610 | return cb(null, node); 611 | } 612 | node.info.master_host = host; 613 | cb(null, node); 614 | }); 615 | } 616 | else { 617 | // Node is a master 618 | cb(null, node); 619 | } 620 | }); 621 | }); 622 | }); 623 | async.parallel(tasks, function(err, nodes) { 624 | if (err) { 625 | self.warn(err); 626 | return self.reorientate(why); 627 | } 628 | var masters = [] 629 | , slaves = [] 630 | , master_host 631 | , master_port 632 | , master_conflict = false 633 | ; 634 | nodes.forEach(function(node) { 635 | if (node.info.role == 'master') { 636 | masters.push(node); 637 | } 638 | else if (node.info.role == 'slave') { 639 | if (master_host && (master_host != node.info.master_host || master_port != node.info.master_port)) { 640 | master_conflict = true; 641 | } 642 | master_host = node.info.master_host; 643 | master_port = node.info.master_port; 644 | slaves.push(node); 645 | } 646 | }); 647 | if (masters.length != 1) { 648 | // Resolve multiple/no masters 649 | self.warn('invalid master count: ' + masters.length); 650 | self.failover(); 651 | } 652 | else if (slaves.length && (master_conflict || master_host != masters[0].host || master_port != masters[0].port)) { 653 | self.warn('master conflict detected'); 654 | self.failover(); 655 | } 656 | else { 657 | self.master = masters.pop(); 658 | self.onReady(); 659 | } 660 | }); 661 | }; 662 | 663 | RedisHAClient.prototype.makeSlave = function(node, cb) { 664 | var self = this; 665 | if (!this.master) { 666 | return self.error("can't make " + node + " a slave of unknown master!"); 667 | } 668 | if (node.host == this.master.host && node.port == this.master.port) { 669 | return self.error('refusing to make ' + node + ' a slave of itself!'); 670 | } 671 | self.log('making ' + node + ' into a slave...'); 672 | node.client.SLAVEOF(this.master.host, this.master.port, function(err, reply) { 673 | if (err) { 674 | return cb(err); 675 | } 676 | node.role = 'slave'; 677 | self.debug(node + ' is slave'); 678 | cb(); 679 | }); 680 | }; 681 | 682 | RedisHAClient.prototype.failover = function() { 683 | var self = this; 684 | if (this.ready) { 685 | return self.warn('ignoring failover while ready'); 686 | } 687 | self.warn('attempting failover!'); 688 | var tasks = []; 689 | var id = uuid(); 690 | self.warn('my failover id: ' + id); 691 | // We can't use a regular EXPIRE call because of a bug in redis which prevents 692 | // slaves from expiring keys correctly. 693 | var was_error = false; 694 | this.up.forEach(function(node) { 695 | tasks.push(function(cb) { 696 | if (self.selected_db == self.options.haredis_db_num) { 697 | // If we are in the haredis_db_num, ignore this. 698 | return cb(); 699 | } 700 | // Switch to the opcounter db. 701 | node.client.SELECT(self.options.haredis_db_num, function(err) { 702 | if (err) { 703 | self.error(err, 'error switching to opcounter db'); 704 | was_error = true; 705 | } 706 | cb(); 707 | }); 708 | }); 709 | tasks.push(function(cb) { 710 | if (was_error) { 711 | return cb(); 712 | } 713 | node.client.MULTI() 714 | .SETNX('haredis:failover', id + ':' + Date.now()) 715 | .GET('haredis:failover', function(err, reply) { 716 | reply = reply.split(':'); 717 | if (reply[0] != id && (Date.now() - reply[1] < self.options.lock_time)) { 718 | self.warn('failover already in progress: ' + reply[0]); 719 | // Don't pass the error, so series() can continue. 720 | was_error = true; 721 | return cb(); 722 | } 723 | }) 724 | .EXEC(function(err, replies) { 725 | if (was_error) { 726 | return; 727 | } 728 | if (err) { 729 | self.error(err, 'error locking node'); 730 | // Don't pass the error, so series() can continue. 731 | was_error = true; 732 | return cb(); 733 | } 734 | else { 735 | // set a shortish ttl on the lock. Note that this doesn't actually work... 736 | node.client.EXPIRE('haredis:failover', 5, function(err) { 737 | if (err) { 738 | self.error(err, 'error setting ttl on lock'); 739 | } 740 | }); 741 | 742 | node.client.GET('haredis:opcounter', function(err, opcounter) { 743 | if (err) { 744 | self.error(err, 'error getting opcounter'); 745 | // Don't pass the error, so parallel() can continue. 746 | was_error = true; 747 | } 748 | else { 749 | node.opcounter = opcounter; 750 | } 751 | cb(null, node); 752 | }); 753 | } 754 | }); 755 | }); 756 | tasks.push(function(cb) { 757 | if (self.selected_db == self.options.haredis_db_num) { 758 | // If we are in the haredis_db_num, ignore this. 759 | return cb(); 760 | } 761 | // Switch to the normal db. 762 | node.client.SELECT(self.selected_db, function(err) { 763 | if (err) { 764 | self.error(err, 'error switching from opcounter db'); 765 | was_error = true; 766 | } 767 | cb(); 768 | }); 769 | }); 770 | }); 771 | async.series(tasks, function(err, results) { 772 | if (results.length) { 773 | if (!was_error) { 774 | self.log('lock was a success!'); 775 | } 776 | else { 777 | results.forEach(function(node) { 778 | if (node && node.client) { 779 | node.client.SELECT(self.options.haredis_db_num, function(err) { 780 | if (err) { 781 | return self.error(err, 'error selecting db to unlock ' + node); 782 | } 783 | node.client.DEL('haredis:failover', function(err) { 784 | if (err) { 785 | return self.error(err, 'error unlocking ' + node); 786 | } 787 | node.client.SELECT(self.selected_db, function(err) { 788 | if (err) { 789 | return self.error(err, 'error unlocking ' + node); 790 | } 791 | self.debug('unlocked ' + node); 792 | }); 793 | }); 794 | }); 795 | } 796 | }); 797 | } 798 | } 799 | if (err) { 800 | self.warn(err); 801 | } 802 | if (was_error) { 803 | return self.reorientate('error during failover'); 804 | } 805 | else { 806 | // We've succeeded in locking all the nodes. Now elect our new master... 807 | var winner; 808 | var candidates = results.filter(function (node) { 809 | return !!node; 810 | }).sort(function (a, b) { 811 | if (a.uptime() > b.uptime()) return -1; 812 | if (b.uptime() > a.uptime()) return 1; 813 | return 0; 814 | }); 815 | candidates.forEach(function (node) { 816 | if (!winner || node.opcounter > winner.opcounter) { 817 | winner = node; 818 | } 819 | }); 820 | if (winner) { 821 | self.warn('elected ' + winner + ' as master!'); 822 | } 823 | else { 824 | return self.reorientate('election had no winner!?'); 825 | } 826 | 827 | winner.client.SLAVEOF('NO', 'ONE', function(err) { 828 | if (err) { 829 | self.error(err, 'error electing master'); 830 | return self.reorientate('error electing master'); 831 | } 832 | self.master = winner; 833 | self.master.role = 'master'; 834 | 835 | var tasks = []; 836 | self.up.forEach(function(node) { 837 | if (!self.isMaster(node)) { 838 | tasks.push(function(cb) { 839 | self.makeSlave(node, cb); 840 | }); 841 | } 842 | }); 843 | if (tasks.length) { 844 | async.parallel(tasks, function(err) { 845 | if (err) { 846 | return self.error(err, 'error making slave!'); 847 | } 848 | self.debug('publishing gossip:master for ' + self.master.toString()); 849 | self.master.client.publish('haredis:gossip:master', self.master.toString()); 850 | self.onReady(); 851 | }); 852 | } 853 | else { 854 | self.onReady(); 855 | } 856 | }); 857 | } 858 | }); 859 | }; 860 | 861 | RedisHAClient.prototype.isMaster = function(node) { 862 | return this.master && this.master.toString() == node.toString(); 863 | }; 864 | 865 | RedisHAClient.prototype.reorientate = function(why) { 866 | var self = this; 867 | this.orientating = false; 868 | self.warn('reorientating (' + why + ') in ' + self.options.reorientate_wait + 'ms'); 869 | this.retryInterval = setTimeout(function() { 870 | self.orientate(why); 871 | }, self.options.reorientate_wait); 872 | }; 873 | 874 | RedisHAClient.prototype.__defineGetter__('slaves', function() { 875 | return this.nodes.filter(function(node) { 876 | return node.role == 'slave' && node.status == 'up'; 877 | }); 878 | }); 879 | 880 | RedisHAClient.prototype.__defineGetter__('responded', function() { 881 | return this.nodes.filter(function(node) { 882 | return node.status != 'initializing'; 883 | }); 884 | }); 885 | 886 | RedisHAClient.prototype.__defineGetter__('up', function() { 887 | return this.nodes.filter(function(node) { 888 | return node.status == 'up'; 889 | }); 890 | }); 891 | 892 | RedisHAClient.prototype.__defineGetter__('down', function() { 893 | return this.nodes.filter(function(node) { 894 | return node.status == 'down'; 895 | }); 896 | }); 897 | 898 | RedisHAClient.prototype.isUp = function(key) { 899 | return this.nodes.some(function(node) { 900 | return node.toString() == key && node.status == 'up'; 901 | }); 902 | }; 903 | 904 | RedisHAClient.prototype.nodeFromKey = function(key) { 905 | return this.nodes.filter(function(node) { 906 | return node.toString() == key; 907 | })[0]; 908 | }; 909 | 910 | RedisHAClient.prototype.randomSlave = function() { 911 | return this.slaves[Math.round(Math.random() * (this.slaves.length - 1))]; 912 | }; 913 | 914 | RedisHAClient.prototype.end = function () { 915 | var self = this, latch = this.nodes.length; 916 | this.nodes.forEach(function (node) { 917 | node.once('end', function () { 918 | if (!--latch) self.emit('end'); 919 | }); 920 | node.end(); 921 | }); 922 | }; 923 | -------------------------------------------------------------------------------- /lib/multi.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis') 2 | , util = require('util') 3 | ; 4 | 5 | function HAMulti(client, args) { 6 | redis.Multi.call(this, client, args); 7 | } 8 | util.inherits(HAMulti, redis.Multi); 9 | 10 | // Defer execution until ready. 11 | HAMulti.prototype.exec = function(callback) { 12 | var self = this; 13 | if (this.client.ready) { 14 | redis.Multi.prototype.exec.call(this, callback); 15 | incrOpcounter(); 16 | } 17 | else { 18 | this.client.once('ready', function() { 19 | redis.Multi.prototype.exec.call(self, callback); 20 | incrOpcounter(); 21 | }); 22 | } 23 | 24 | // Increment to opcounter for every write operation in the queue. 25 | function incrOpcounter() { 26 | var incr = 0; 27 | self.queue.forEach(function(args) { 28 | if (!self.client.isRead(args[0])) { 29 | incr++; 30 | } 31 | }); 32 | if (incr) { 33 | self.client.incrOpcounter(incr, function(err) { 34 | if (err) { 35 | self.client.master.emit('error', err); 36 | } 37 | }); 38 | } 39 | } 40 | }; 41 | 42 | module.exports = HAMulti; -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis') 2 | , util = require('util') 3 | , EventEmitter = require('events').EventEmitter 4 | , dns = require('dns') 5 | , async = require('async') 6 | ; 7 | 8 | function Node(spec, options) { 9 | var self = this; 10 | this.host = spec.host; 11 | this.port = spec.port; 12 | this.clients = []; 13 | this.upSince = null; 14 | 15 | options = options || {}; 16 | if (typeof options.no_ready_check == 'undefined') { 17 | options.no_ready_check = true; 18 | } 19 | this.options = {}; 20 | Object.keys(options).forEach(function(k) { 21 | self.options[k] = options[k]; 22 | }); 23 | 24 | this.setStatus('initializing'); 25 | 26 | if (this.host.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { 27 | this.connect(); 28 | } 29 | else { 30 | // Resolve host, to prevent false duplication 31 | Node.resolveHost(this.host, function(err, host) { 32 | if (err) { 33 | return self.emit('error', err); 34 | } 35 | self.host = host; 36 | self.connect(); 37 | }); 38 | } 39 | } 40 | util.inherits(Node, EventEmitter); 41 | 42 | // Static method 43 | Node.resolveHost = function(host, cb) { 44 | dns.lookup(host, 4, cb); 45 | }; 46 | 47 | Node.parseInfo = function(reply) { 48 | var lines = reply.toString().split("\r\n"), obj = {}; 49 | 50 | lines.forEach(function(line) { 51 | var parts = line.split(':'); 52 | if (parts[1]) { 53 | obj[parts[0]] = parts[1]; 54 | } 55 | }); 56 | 57 | obj.versions = []; 58 | obj.redis_version.split('.').forEach(function(num) { 59 | obj.versions.push(+num); 60 | }); 61 | return obj; 62 | }; 63 | 64 | Node.prototype.connect = function() { 65 | var self = this; 66 | this.client = redis.createClient(this.port, this.host, this.options); 67 | if (this.auth_pass) { 68 | this.client.auth(this.auth_pass); 69 | } 70 | this.clients.push(this.client); 71 | this.client.on('error', function(err) { 72 | self.emit('error', err); 73 | }); 74 | this.client.on('end', function() { 75 | if (!this.closing) { 76 | self.setStatus('reconnecting'); 77 | // Client connection closed without quit(). If reconnection fails, the 78 | // node is considered down. 79 | setTimeout(function waitForReconnect() { 80 | if (!self.client.connected) { 81 | self.emit('error', new Error(self + ' connection dropped and reconnection failed!')); 82 | self.setStatus('down'); 83 | } 84 | }, Math.floor(this.retry_delay * this.retry_backoff * 2)); 85 | } 86 | }); 87 | this.client.on('connect', function() { 88 | if (self.options.single_mode) { 89 | // No failover possible, don't check for slave-read-only 90 | self.setStatus('up'); 91 | } 92 | else { 93 | self.client.CONFIG('GET', 'slave-read-only', function(err, reply) { 94 | if (err) { 95 | return self.emit('error', err); 96 | } 97 | if (reply[1] !== 'yes') { 98 | self.setStatus('up'); 99 | } 100 | else { 101 | console.warn('WARNING! ' + self + ' has slave-read-only mode ON, which breaks haredis failover! slave-read-only automatically turned OFF.'); 102 | self.client.CONFIG('SET', 'slave-read-only', 'no', function(err, reply) { 103 | if (err) { 104 | return self.emit('error', err); 105 | } 106 | self.setStatus('up'); 107 | }); 108 | } 109 | }); 110 | } 111 | }); 112 | this.client.on('monitor', function(time, args) { 113 | self.emit('monitor', time, args); 114 | }); 115 | 116 | // Maintain a separate pub/sub client. 117 | this.subClient = redis.createClient(this.port, this.host, this.options); 118 | if (this.auth_pass) { 119 | this.subClient.auth(this.auth_pass); 120 | } 121 | this.clients.push(this.subClient); 122 | this.subClient.on('error', function(err) { 123 | self.emit('error', err); 124 | }); 125 | this.subClient.on('connect', function() { 126 | // UGLY HACK: node_redis does not restore pub/sub subscriptions correctly upon 127 | // reconnect. In fact, the client crashes! Resubscription will be handled at the RedisHAClient level. 128 | // @see https://github.com/mranney/node_redis/issues/191 129 | this.subscribe('haredis:gossip:master'); 130 | this.subscription_set = {}; 131 | }); 132 | 133 | // Subtract 1 from all counts, for the gossip channel we've set up. 134 | this.subClient.on('subscribe', function(channel, count) { 135 | self.emit('subscribe', channel, count - 1); 136 | }); 137 | this.subClient.on('unsubscribe', function(channel, count) { 138 | self.emit('unsubscribe', channel, count - 1); 139 | }); 140 | this.subClient.on('message', function(channel, message) { 141 | self.emit('message', channel, message); 142 | }); 143 | this.subClient.on('psubscribe', function(pattern, count) { 144 | self.emit('psubscribe', pattern, count - 1); 145 | }); 146 | this.subClient.on('punsubscribe', function(pattern, count) { 147 | self.emit('punsubscribe', pattern, count - 1); 148 | }); 149 | this.subClient.on('pmessage', function(pattern, channel, message) { 150 | self.emit('pmessage', pattern, channel, message); 151 | }); 152 | }; 153 | 154 | Node.prototype.quit = function(callback) { 155 | var tasks = []; 156 | 157 | this.clients.forEach(function(client) { 158 | tasks.push(function(cb) { 159 | client.quit(cb); 160 | }); 161 | }); 162 | async.parallel(tasks, function(err, replies) { 163 | if (callback) { 164 | callback(err, replies); 165 | } 166 | }); 167 | }; 168 | 169 | Node.prototype.setStatus = function(status) { 170 | if (status != this.status) { 171 | if (status === 'up') this.upSince = Date.now(); 172 | else this.upSince = null; 173 | this.status = status; 174 | this.emit(status); 175 | } 176 | }; 177 | 178 | Node.prototype.toString = function() { 179 | return this.host + ':' + this.port; 180 | }; 181 | 182 | Node.prototype.uptime = function () { 183 | if (this.upSince === null) return null; 184 | return Date.now() - this.upSince; 185 | }; 186 | 187 | Node.prototype.end = function () { 188 | this.clients.forEach(function (client) { 189 | client.end(); 190 | }); 191 | this.emit('end'); 192 | }; 193 | 194 | module.exports = Node; 195 | -------------------------------------------------------------------------------- /lib/uuid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * uuid generator 3 | * -------------- 4 | * 5 | * @exports {Function} uuid generator function 6 | */ 7 | 8 | /** 9 | * @param [len] {Number} Length of the ID to generate. 10 | * @return {String} A unique alphanumeric string. 11 | */ 12 | module.exports = function(len) { 13 | len = (len || 8); 14 | var ret = '' 15 | , choices = 'ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwyxz0123456789' 16 | , range = choices.length - 1 17 | , len_left = len 18 | ; 19 | while (len_left--) { 20 | ret += choices[Math.round(Math.random() * range)]; 21 | } 22 | return ret; 23 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Carlos Rodriguez (http://s8f.org/)", 3 | "name": "haredis", 4 | "description": "High-availability redis in Node.js", 5 | "version": "0.2.15", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/carlos8f/haredis.git" 9 | }, 10 | "main": "./lib", 11 | "scripts": { 12 | "test": "make test" 13 | }, 14 | "dependencies": { 15 | "redis": "~0.7.2", 16 | "async": "~0.1.22", 17 | "pkginfo": "~0.2.3" 18 | }, 19 | "devDependencies": { 20 | "mocha": "~1.9.0", 21 | "haredis-tmp": "~0.1.0" 22 | }, 23 | "optionalDependencies": {}, 24 | "engines": { 25 | "node": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test-singlemode.js: -------------------------------------------------------------------------------- 1 | /*global require console setTimeout process Buffer */ 2 | var redis = require("./index"), 3 | client = redis.createClient(), 4 | client2 = redis.createClient(), 5 | client3 = redis.createClient(), 6 | assert = require("assert"), 7 | util = require("util"), 8 | test_db_num = 15, // this DB will be flushed and used for testing 9 | tests = {}, 10 | connected = false, 11 | ended = false, 12 | next, cur_start, run_next_test, all_tests, all_start, test_count; 13 | 14 | // Set this to truthy to see the wire protocol and other debugging info 15 | redis.debug_mode = process.argv[2]; 16 | 17 | function buffers_to_strings(arr) { 18 | return arr.map(function (val) { 19 | return val.toString(); 20 | }); 21 | } 22 | 23 | function require_number(expected, label) { 24 | return function (err, results) { 25 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 26 | assert.strictEqual(expected, results, label + " " + expected + " !== " + results); 27 | assert.strictEqual(typeof results, "number", label); 28 | return true; 29 | }; 30 | } 31 | 32 | function require_number_any(label) { 33 | return function (err, results) { 34 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 35 | assert.strictEqual(typeof results, "number", label + " " + results + " is not a number"); 36 | return true; 37 | }; 38 | } 39 | 40 | function require_number_pos(label) { 41 | return function (err, results) { 42 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 43 | assert.strictEqual(true, (results > 0), label + " " + results + " is not a positive number"); 44 | return true; 45 | }; 46 | } 47 | 48 | function require_string(str, label) { 49 | return function (err, results) { 50 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 51 | assert.equal(str, results, label + " " + str + " does not match " + results); 52 | return true; 53 | }; 54 | } 55 | 56 | function require_null(label) { 57 | return function (err, results) { 58 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 59 | assert.strictEqual(null, results, label + ": " + results + " is not null"); 60 | return true; 61 | }; 62 | } 63 | 64 | function require_error(label) { 65 | return function (err, results) { 66 | assert.notEqual(err, null, label + " err is null, but an error is expected here."); 67 | return true; 68 | }; 69 | } 70 | 71 | function is_empty_array(obj) { 72 | return Array.isArray(obj) && obj.length === 0; 73 | } 74 | 75 | function last(name, fn) { 76 | return function (err, results) { 77 | fn(err, results); 78 | next(name); 79 | }; 80 | } 81 | 82 | next = function next(name) { 83 | console.log(" \x1b[33m" + (Date.now() - cur_start) + "\x1b[0m ms"); 84 | run_next_test(); 85 | }; 86 | 87 | // Tests are run in the order they are defined. So FLUSHDB should be stay first. 88 | 89 | tests.FLUSHDB = function () { 90 | var name = "FLUSHDB"; 91 | client.select(test_db_num, require_string("OK", name)); 92 | client2.select(test_db_num, require_string("OK", name)); 93 | client3.select(test_db_num, require_string("OK", name)); 94 | client.mset("flush keys 1", "flush val 1", "flush keys 2", "flush val 2", require_string("OK", name)); 95 | client.FLUSHDB(require_string("OK", name)); 96 | client.dbsize(last(name, require_number(0, name))); 97 | }; 98 | 99 | tests.MULTI_1 = function () { 100 | var name = "MULTI_1", multi1, multi2; 101 | 102 | // Provoke an error at queue time 103 | multi1 = client.multi(); 104 | multi1.mset("multifoo", "10", "multibar", "20", require_string("OK", name)); 105 | // newer server version cancels transaction with EXECABORT, so disable this test. 106 | //multi1.set("foo2", require_error(name)); 107 | multi1.incr("multifoo", require_number(11, name)); 108 | multi1.incr("multibar", require_number(21, name)); 109 | multi1.exec(); 110 | 111 | // Confirm that the previous command, while containing an error, still worked. 112 | multi2 = client.multi(); 113 | multi2.incr("multibar", require_number(22, name)); 114 | multi2.incr("multifoo", require_number(12, name)); 115 | multi2.exec(function (err, replies) { 116 | assert.strictEqual(22, replies[0]); 117 | assert.strictEqual(12, replies[1]); 118 | next(name); 119 | }); 120 | }; 121 | 122 | tests.MULTI_2 = function () { 123 | var name = "MULTI_2"; 124 | 125 | // test nested multi-bulk replies 126 | client.multi([ 127 | ["mget", "multifoo", "multibar", function (err, res) { 128 | assert.strictEqual(2, res.length, name); 129 | assert.strictEqual("12", res[0].toString(), name); 130 | assert.strictEqual("22", res[1].toString(), name); 131 | }], 132 | // newer server version cancels transaction with EXECABORT, so disable this test. 133 | //["set", "foo2", require_error(name)], 134 | ["incr", "multifoo", require_number(13, name)], 135 | ["incr", "multibar", require_number(23, name)] 136 | ]).exec(function (err, replies) { 137 | assert.strictEqual(2, replies[0].length, name); 138 | assert.strictEqual("12", replies[0][0].toString(), name); 139 | assert.strictEqual("22", replies[0][1].toString(), name); 140 | 141 | assert.strictEqual("13", replies[1].toString()); 142 | assert.strictEqual("23", replies[2].toString()); 143 | next(name); 144 | }); 145 | }; 146 | 147 | tests.MULTI_3 = function () { 148 | var name = "MULTI_3"; 149 | 150 | client.sadd("some set", "mem 1"); 151 | client.sadd("some set", "mem 2"); 152 | client.sadd("some set", "mem 3"); 153 | client.sadd("some set", "mem 4"); 154 | 155 | // make sure empty mb reply works 156 | client.del("some missing set"); 157 | client.smembers("some missing set", function (err, reply) { 158 | // make sure empty mb reply works 159 | assert.strictEqual(true, is_empty_array(reply), name); 160 | }); 161 | 162 | // test nested multi-bulk replies with empty mb elements. 163 | client.multi([ 164 | ["smembers", "some set"], 165 | ["del", "some set"], 166 | ["smembers", "some set"] 167 | ]) 168 | .scard("some set") 169 | .exec(function (err, replies) { 170 | assert.strictEqual(true, is_empty_array(replies[2]), name); 171 | next(name); 172 | }); 173 | }; 174 | 175 | tests.MULTI_4 = function () { 176 | var name = "MULTI_4"; 177 | 178 | client.multi() 179 | .mset('some', '10', 'keys', '20') 180 | .incr('some') 181 | .incr('keys') 182 | .mget('some', 'keys') 183 | .exec(function (err, replies) { 184 | assert.strictEqual(null, err); 185 | assert.equal('OK', replies[0]); 186 | assert.equal(11, replies[1]); 187 | assert.equal(21, replies[2]); 188 | assert.equal(11, replies[3][0].toString()); 189 | assert.equal(21, replies[3][1].toString()); 190 | next(name); 191 | }); 192 | }; 193 | 194 | tests.MULTI_5 = function () { 195 | var name = "MULTI_5"; 196 | 197 | // test nested multi-bulk replies with nulls. 198 | client.multi([ 199 | ["mget", ["multifoo", "some", "random value", "keys"]], 200 | ["incr", "multifoo"] 201 | ]) 202 | .exec(function (err, replies) { 203 | assert.strictEqual(replies.length, 2, name); 204 | assert.strictEqual(replies[0].length, 4, name); 205 | next(name); 206 | }); 207 | }; 208 | 209 | tests.MULTI_6 = function () { 210 | var name = "MULTI_6"; 211 | 212 | client.multi() 213 | .hmset("multihash", "a", "foo", "b", 1) 214 | .hmset("multihash", { 215 | extra: "fancy", 216 | things: "here" 217 | }) 218 | .hgetall("multihash") 219 | .exec(function (err, replies) { 220 | assert.strictEqual(null, err); 221 | assert.equal("OK", replies[0]); 222 | assert.equal(Object.keys(replies[2]).length, 4); 223 | assert.equal("foo", replies[2].a); 224 | assert.equal("1", replies[2].b); 225 | assert.equal("fancy", replies[2].extra); 226 | assert.equal("here", replies[2].things); 227 | next(name); 228 | }); 229 | }; 230 | 231 | tests.EVAL_1 = function () { 232 | var name = "EVAL_1"; 233 | 234 | if (client.server_info.versions[0] >= 2 && client.server_info.versions[1] >= 9) { 235 | // test {EVAL - Lua integer -> Redis protocol type conversion} 236 | client.eval("return 100.5", 0, require_number(100, name)); 237 | // test {EVAL - Lua string -> Redis protocol type conversion} 238 | client.eval("return 'hello world'", 0, require_string("hello world", name)); 239 | // test {EVAL - Lua true boolean -> Redis protocol type conversion} 240 | client.eval("return true", 0, require_number(1, name)); 241 | // test {EVAL - Lua false boolean -> Redis protocol type conversion} 242 | client.eval("return false", 0, require_null(name)); 243 | // test {EVAL - Lua status code reply -> Redis protocol type conversion} 244 | client.eval("return {ok='fine'}", 0, require_string("fine", name)); 245 | // test {EVAL - Lua error reply -> Redis protocol type conversion} 246 | client.eval("return {err='this is an error'}", 0, require_error(name)); 247 | // test {EVAL - Lua table -> Redis protocol type conversion} 248 | client.eval("return {1,2,3,'ciao',{1,2}}", 0, function (err, res) { 249 | assert.strictEqual(5, res.length, name); 250 | assert.strictEqual(1, res[0], name); 251 | assert.strictEqual(2, res[1], name); 252 | assert.strictEqual(3, res[2], name); 253 | assert.strictEqual("ciao", res[3], name); 254 | assert.strictEqual(2, res[4].length, name); 255 | assert.strictEqual(1, res[4][0], name); 256 | assert.strictEqual(2, res[4][1], name); 257 | }); 258 | // test {EVAL - Are the KEYS and ARGS arrays populated correctly?} 259 | client.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "a", "b", "c", "d", function (err, res) { 260 | assert.strictEqual(4, res.length, name); 261 | assert.strictEqual("a", res[0], name); 262 | assert.strictEqual("b", res[1], name); 263 | assert.strictEqual("c", res[2], name); 264 | assert.strictEqual("d", res[3], name); 265 | }); 266 | // test {EVAL - is Lua able to call Redis API?} 267 | client.set("mykey", "myval"); 268 | client.eval("return redis.call('get','mykey')", 0, require_string("myval", name)); 269 | // test {EVALSHA - Can we call a SHA1 if already defined?} 270 | client.evalsha("9bd632c7d33e571e9f24556ebed26c3479a87129", 0, require_string("myval", name)); 271 | // test {EVALSHA - Do we get an error on non defined SHA1?} 272 | client.evalsha("ffffffffffffffffffffffffffffffffffffffff", 0, require_error(name)); 273 | // test {EVAL - Redis integer -> Lua type conversion} 274 | client.set("x", 0); 275 | client.eval("local foo = redis.call('incr','x')\n" + "return {type(foo),foo}", 0, function (err, res) { 276 | assert.strictEqual(2, res.length, name); 277 | assert.strictEqual("number", res[0], name); 278 | assert.strictEqual(1, res[1], name); 279 | }); 280 | // test {EVAL - Redis bulk -> Lua type conversion} 281 | client.eval("local foo = redis.call('get','mykey'); return {type(foo),foo}", 0, function (err, res) { 282 | assert.strictEqual(2, res.length, name); 283 | assert.strictEqual("string", res[0], name); 284 | assert.strictEqual("myval", res[1], name); 285 | }); 286 | // test {EVAL - Redis multi bulk -> Lua type conversion} 287 | client.del("mylist"); 288 | client.rpush("mylist", "a"); 289 | client.rpush("mylist", "b"); 290 | client.rpush("mylist", "c"); 291 | client.eval("local foo = redis.call('lrange','mylist',0,-1)\n" + "return {type(foo),foo[1],foo[2],foo[3],# foo}", 0, function (err, res) { 292 | assert.strictEqual(5, res.length, name); 293 | assert.strictEqual("table", res[0], name); 294 | assert.strictEqual("a", res[1], name); 295 | assert.strictEqual("b", res[2], name); 296 | assert.strictEqual("c", res[3], name); 297 | assert.strictEqual(3, res[4], name); 298 | }); 299 | // test {EVAL - Redis status reply -> Lua type conversion} 300 | client.eval("local foo = redis.call('set','mykey','myval'); return {type(foo),foo['ok']}", 0, function (err, res) { 301 | assert.strictEqual(2, res.length, name); 302 | assert.strictEqual("table", res[0], name); 303 | assert.strictEqual("OK", res[1], name); 304 | }); 305 | // test {EVAL - Redis error reply -> Lua type conversion} 306 | client.set("mykey", "myval"); 307 | client.eval("local foo = redis.call('incr','mykey'); return {type(foo),foo['err']}", 0, function (err, res) { 308 | assert.strictEqual(2, res.length, name); 309 | assert.strictEqual("table", res[0], name); 310 | assert.strictEqual("ERR value is not an integer or out of range", res[1], name); 311 | }); 312 | // test {EVAL - Redis nil bulk reply -> Lua type conversion} 313 | client.del("mykey"); 314 | client.eval("local foo = redis.call('get','mykey'); return {type(foo),foo == false}", 0, function (err, res) { 315 | assert.strictEqual(2, res.length, name); 316 | assert.strictEqual("boolean", res[0], name); 317 | assert.strictEqual(1, res[1], name); 318 | }); 319 | // test {EVAL - Script can't run more than configured time limit} { 320 | client.config("set", "lua-time-limit", 1); 321 | client.eval("local i = 0; while true do i=i+1 end", 0, last("name", require_error(name))); 322 | } else { 323 | console.log("Skipping " + name + " because server version isn't new enough."); 324 | next(name); 325 | } 326 | }; 327 | 328 | tests.WATCH_MULTI = function () { 329 | var name = 'WATCH_MULTI', multi; 330 | 331 | if (client.server_info.versions[0] >= 2 && client.server_info.versions[1] >= 1) { 332 | client.watch(name); 333 | client.incr(name); 334 | multi = client.multi(); 335 | multi.incr(name); 336 | multi.exec(last(name, require_null(name))); 337 | } else { 338 | console.log("Skipping " + name + " because server version isn't new enough."); 339 | next(name); 340 | } 341 | }; 342 | 343 | tests.detect_buffers = function () { 344 | var name = "detect_buffers", detect_client = redis.createClient(null, null, {detect_buffers: true}); 345 | 346 | detect_client.on("ready", function () { 347 | // single Buffer or String 348 | detect_client.set("string key 1", "string value"); 349 | detect_client.get("string key 1", require_string("string value", name)); 350 | detect_client.get(new Buffer("string key 1"), function (err, reply) { 351 | assert.strictEqual(null, err, name); 352 | assert.strictEqual(true, Buffer.isBuffer(reply), name); 353 | assert.strictEqual("", reply.inspect(), name); 354 | }); 355 | 356 | detect_client.hmset("hash key 2", "key 1", "val 1", "key 2", "val 2"); 357 | // array of Buffers or Strings 358 | detect_client.hmget("hash key 2", "key 1", "key 2", function (err, reply) { 359 | assert.strictEqual(null, err, name); 360 | assert.strictEqual(true, Array.isArray(reply), name); 361 | assert.strictEqual(2, reply.length, name); 362 | assert.strictEqual("val 1", reply[0], name); 363 | assert.strictEqual("val 2", reply[1], name); 364 | }); 365 | detect_client.hmget(new Buffer("hash key 2"), "key 1", "key 2", function (err, reply) { 366 | assert.strictEqual(null, err, name); 367 | assert.strictEqual(true, Array.isArray(reply)); 368 | assert.strictEqual(2, reply.length, name); 369 | assert.strictEqual(true, Buffer.isBuffer(reply[0])); 370 | assert.strictEqual(true, Buffer.isBuffer(reply[1])); 371 | assert.strictEqual("", reply[0].inspect(), name); 372 | assert.strictEqual("", reply[1].inspect(), name); 373 | }); 374 | 375 | // Object of Buffers or Strings 376 | detect_client.hgetall("hash key 2", function (err, reply) { 377 | assert.strictEqual(null, err, name); 378 | assert.strictEqual("object", typeof reply, name); 379 | assert.strictEqual(2, Object.keys(reply).length, name); 380 | assert.strictEqual("val 1", reply["key 1"], name); 381 | assert.strictEqual("val 2", reply["key 2"], name); 382 | }); 383 | detect_client.hgetall(new Buffer("hash key 2"), function (err, reply) { 384 | assert.strictEqual(null, err, name); 385 | assert.strictEqual("object", typeof reply, name); 386 | assert.strictEqual(2, Object.keys(reply).length, name); 387 | assert.strictEqual(true, Buffer.isBuffer(reply["key 1"])); 388 | assert.strictEqual(true, Buffer.isBuffer(reply["key 2"])); 389 | assert.strictEqual("", reply["key 1"].inspect(), name); 390 | assert.strictEqual("", reply["key 2"].inspect(), name); 391 | }); 392 | 393 | detect_client.quit(function (err, res) { 394 | next(name); 395 | }); 396 | }); 397 | }; 398 | 399 | tests.socket_nodelay = function () { 400 | var name = "socket_nodelay", c1, c2, c3, ready_count = 0, quit_count = 0; 401 | 402 | c1 = redis.createClient(null, null, {socket_nodelay: true}); 403 | c2 = redis.createClient(null, null, {socket_nodelay: false}); 404 | c3 = redis.createClient(null, null); 405 | 406 | function quit_check() { 407 | quit_count++; 408 | 409 | if (quit_count === 3) { 410 | next(name); 411 | } 412 | } 413 | 414 | function run() { 415 | assert.strictEqual(true, c1.options.socket_nodelay, name); 416 | assert.strictEqual(false, c2.options.socket_nodelay, name); 417 | assert.strictEqual(true, c3.options.socket_nodelay, name); 418 | 419 | c1.set(["set key 1", "set val"], require_string("OK", name)); 420 | c1.set(["set key 2", "set val"], require_string("OK", name)); 421 | c1.get(["set key 1"], require_string("set val", name)); 422 | c1.get(["set key 2"], require_string("set val", name)); 423 | 424 | c2.set(["set key 3", "set val"], require_string("OK", name)); 425 | c2.set(["set key 4", "set val"], require_string("OK", name)); 426 | c2.get(["set key 3"], require_string("set val", name)); 427 | c2.get(["set key 4"], require_string("set val", name)); 428 | 429 | c3.set(["set key 5", "set val"], require_string("OK", name)); 430 | c3.set(["set key 6", "set val"], require_string("OK", name)); 431 | c3.get(["set key 5"], require_string("set val", name)); 432 | c3.get(["set key 6"], require_string("set val", name)); 433 | 434 | c1.quit(quit_check); 435 | c2.quit(quit_check); 436 | c3.quit(quit_check); 437 | } 438 | 439 | function ready_check() { 440 | ready_count++; 441 | if (ready_count === 3) { 442 | run(); 443 | } 444 | } 445 | 446 | c1.on("ready", ready_check); 447 | c2.on("ready", ready_check); 448 | c3.on("ready", ready_check); 449 | }; 450 | 451 | tests.reconnect = function () { 452 | var name = "reconnect"; 453 | 454 | client.set("recon 1", "one"); 455 | client.set("recon 2", "two", function (err, res) { 456 | // Do not do this in normal programs. This is to simulate the server closing on us. 457 | // For orderly shutdown in normal programs, do client.quit() 458 | client.stream.destroy(); 459 | }); 460 | 461 | client.on("reconnecting", function on_recon(params) { 462 | client.on("connect", function on_connect() { 463 | client.select(test_db_num, require_string("OK", name)); 464 | client.get("recon 1", require_string("one", name)); 465 | client.get("recon 1", require_string("one", name)); 466 | client.get("recon 2", require_string("two", name)); 467 | client.get("recon 2", require_string("two", name)); 468 | client.removeListener("connect", on_connect); 469 | client.removeListener("reconnecting", on_recon); 470 | next(name); 471 | }); 472 | }); 473 | }; 474 | 475 | tests.HSET = function () { 476 | var key = "test hash", 477 | field1 = new Buffer("0123456789"), 478 | value1 = new Buffer("abcdefghij"), 479 | field2 = new Buffer(0), 480 | value2 = new Buffer(0), 481 | name = "HSET"; 482 | 483 | client.HSET(key, field1, value1, require_number(1, name)); 484 | client.HGET(key, field1, require_string(value1.toString(), name)); 485 | 486 | // Empty value 487 | client.HSET(key, field1, value2, require_number(0, name)); 488 | client.HGET([key, field1], require_string("", name)); 489 | 490 | // Empty key, empty value 491 | client.HSET([key, field2, value1], require_number(1, name)); 492 | client.HSET(key, field2, value2, last(name, require_number(0, name))); 493 | }; 494 | 495 | tests.HMSET_BUFFER_AND_ARRAY = function () { 496 | // Saving a buffer and an array to the same key should not error 497 | var key = "test hash", 498 | field1 = "buffer", 499 | value1 = new Buffer("abcdefghij"), 500 | field2 = "array", 501 | value2 = ["array contents"], 502 | name = "HSET"; 503 | 504 | client.HMSET(key, field1, value1, field2, value2, last(name, require_string("OK", name))); 505 | }; 506 | 507 | // TODO - add test for HMSET with optional callbacks 508 | 509 | tests.HMGET = function () { 510 | var key1 = "test hash 1", key2 = "test hash 2", name = "HMGET"; 511 | 512 | // redis-like hmset syntax 513 | client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value", require_string("OK", name)); 514 | 515 | // fancy hmset syntax 516 | client.HMSET(key2, { 517 | "0123456789": "abcdefghij", 518 | "some manner of key": "a type of value" 519 | }, require_string("OK", name)); 520 | 521 | client.HMGET(key1, "0123456789", "some manner of key", function (err, reply) { 522 | assert.strictEqual("abcdefghij", reply[0].toString(), name); 523 | assert.strictEqual("a type of value", reply[1].toString(), name); 524 | }); 525 | 526 | client.HMGET(key2, "0123456789", "some manner of key", function (err, reply) { 527 | assert.strictEqual("abcdefghij", reply[0].toString(), name); 528 | assert.strictEqual("a type of value", reply[1].toString(), name); 529 | }); 530 | 531 | client.HMGET(key1, ["0123456789"], function (err, reply) { 532 | assert.strictEqual("abcdefghij", reply[0], name); 533 | }); 534 | 535 | client.HMGET(key1, ["0123456789", "some manner of key"], function (err, reply) { 536 | assert.strictEqual("abcdefghij", reply[0], name); 537 | assert.strictEqual("a type of value", reply[1], name); 538 | }); 539 | 540 | client.HMGET(key1, "missing thing", "another missing thing", function (err, reply) { 541 | assert.strictEqual(null, reply[0], name); 542 | assert.strictEqual(null, reply[1], name); 543 | next(name); 544 | }); 545 | }; 546 | 547 | tests.HINCRBY = function () { 548 | var name = "HINCRBY"; 549 | client.hset("hash incr", "value", 10, require_number(1, name)); 550 | client.HINCRBY("hash incr", "value", 1, require_number(11, name)); 551 | client.HINCRBY("hash incr", "value 2", 1, last(name, require_number(1, name))); 552 | }; 553 | 554 | tests.SUBSCRIBE = function () { 555 | var client1 = client, msg_count = 0, name = "SUBSCRIBE"; 556 | 557 | client1.on("subscribe", function (channel, count) { 558 | if (channel === "chan1") { 559 | client2.publish("chan1", "message 1", require_number(1, name)); 560 | client2.publish("chan2", "message 2", require_number(1, name)); 561 | client2.publish("chan1", "message 3", require_number(1, name)); 562 | } 563 | }); 564 | 565 | client1.on("unsubscribe", function (channel, count) { 566 | if (count === 0) { 567 | // make sure this connection can go into and out of pub/sub mode 568 | client1.incr("did a thing", last(name, require_number(2, name))); 569 | } 570 | }); 571 | 572 | client1.on("message", function (channel, message) { 573 | msg_count += 1; 574 | assert.strictEqual("message " + msg_count, message.toString()); 575 | if (msg_count === 3) { 576 | client1.unsubscribe("chan1", "chan2"); 577 | } 578 | }); 579 | 580 | client1.set("did a thing", 1, require_string("OK", name)); 581 | client1.subscribe("chan1", "chan2", function (err, results) { 582 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 583 | assert.strictEqual("chan1", results.toString(), name); 584 | }); 585 | }; 586 | 587 | tests.SUBSCRIBE_QUIT = function () { 588 | var name = "SUBSCRIBE_QUIT"; 589 | client3.on("end", function () { 590 | next(name); 591 | }); 592 | client3.on("subscribe", function (channel, count) { 593 | client3.quit(); 594 | }); 595 | client3.subscribe("chan3"); 596 | }; 597 | 598 | tests.EXISTS = function () { 599 | var name = "EXISTS"; 600 | client.del("foo", "foo2", require_number_any(name)); 601 | client.set("foo", "bar", require_string("OK", name)); 602 | client.EXISTS("foo", require_number(1, name)); 603 | client.EXISTS("foo2", last(name, require_number(0, name))); 604 | }; 605 | 606 | tests.DEL = function () { 607 | var name = "DEL"; 608 | client.DEL("delkey", require_number_any(name)); 609 | client.set("delkey", "delvalue", require_string("OK", name)); 610 | client.DEL("delkey", require_number(1, name)); 611 | client.exists("delkey", require_number(0, name)); 612 | client.DEL("delkey", require_number(0, name)); 613 | client.mset("delkey", "delvalue", "delkey2", "delvalue2", require_string("OK", name)); 614 | client.DEL("delkey", "delkey2", last(name, require_number(2, name))); 615 | }; 616 | 617 | tests.TYPE = function () { 618 | var name = "TYPE"; 619 | client.set(["string key", "should be a string"], require_string("OK", name)); 620 | client.rpush(["list key", "should be a list"], require_number_pos(name)); 621 | client.sadd(["set key", "should be a set"], require_number_any(name)); 622 | client.zadd(["zset key", "10.0", "should be a zset"], require_number_any(name)); 623 | client.hset(["hash key", "hashtest", "should be a hash"], require_number_any(0, name)); 624 | 625 | client.TYPE(["string key"], require_string("string", name)); 626 | client.TYPE(["list key"], require_string("list", name)); 627 | client.TYPE(["set key"], require_string("set", name)); 628 | client.TYPE(["zset key"], require_string("zset", name)); 629 | client.TYPE("not here yet", require_string("none", name)); 630 | client.TYPE(["hash key"], last(name, require_string("hash", name))); 631 | }; 632 | 633 | tests.KEYS = function () { 634 | var name = "KEYS"; 635 | client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], require_string("OK", name)); 636 | client.KEYS(["test keys*"], function (err, results) { 637 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 638 | assert.strictEqual(2, results.length, name); 639 | results.sort(); 640 | assert.strictEqual("test keys 1", results[0].toString(), name); 641 | assert.strictEqual("test keys 2", results[1].toString(), name); 642 | next(name); 643 | }); 644 | }; 645 | 646 | tests.MULTIBULK_ZERO_LENGTH = function () { 647 | var name = "MULTIBULK_ZERO_LENGTH"; 648 | client.KEYS(['users:*'], function (err, results) { 649 | assert.strictEqual(null, err, 'error on empty multibulk reply'); 650 | assert.strictEqual(true, is_empty_array(results), "not an empty array"); 651 | next(name); 652 | }); 653 | }; 654 | 655 | tests.RANDOMKEY = function () { 656 | var name = "RANDOMKEY"; 657 | client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], require_string("OK", name)); 658 | client.RANDOMKEY([], function (err, results) { 659 | assert.strictEqual(null, err, name + " result sent back unexpected error: " + err); 660 | assert.strictEqual(true, /\w+/.test(results), name); 661 | next(name); 662 | }); 663 | }; 664 | 665 | tests.RENAME = function () { 666 | var name = "RENAME"; 667 | client.set(['foo', 'bar'], require_string("OK", name)); 668 | client.RENAME(["foo", "new foo"], require_string("OK", name)); 669 | client.exists(["foo"], require_number(0, name)); 670 | client.exists(["new foo"], last(name, require_number(1, name))); 671 | }; 672 | 673 | tests.RENAMENX = function () { 674 | var name = "RENAMENX"; 675 | client.set(['foo', 'bar'], require_string("OK", name)); 676 | client.set(['foo2', 'bar2'], require_string("OK", name)); 677 | client.RENAMENX(["foo", "foo2"], require_number(0, name)); 678 | client.exists(["foo"], require_number(1, name)); 679 | client.exists(["foo2"], require_number(1, name)); 680 | client.del(["foo2"], require_number(1, name)); 681 | client.RENAMENX(["foo", "foo2"], require_number(1, name)); 682 | client.exists(["foo"], require_number(0, name)); 683 | client.exists(["foo2"], last(name, require_number(1, name))); 684 | }; 685 | 686 | tests.DBSIZE = function () { 687 | var name = "DBSIZE"; 688 | client.set(['foo', 'bar'], require_string("OK", name)); 689 | client.DBSIZE([], last(name, require_number_pos("DBSIZE"))); 690 | }; 691 | 692 | tests.GET = function () { 693 | var name = "GET"; 694 | client.set(["get key", "get val"], require_string("OK", name)); 695 | client.GET(["get key"], last(name, require_string("get val", name))); 696 | }; 697 | 698 | tests.SET = function () { 699 | var name = "SET"; 700 | client.SET(["set key", "set val"], require_string("OK", name)); 701 | client.get(["set key"], last(name, require_string("set val", name))); 702 | }; 703 | 704 | tests.GETSET = function () { 705 | var name = "GETSET"; 706 | client.set(["getset key", "getset val"], require_string("OK", name)); 707 | client.GETSET(["getset key", "new getset val"], require_string("getset val", name)); 708 | client.get(["getset key"], last(name, require_string("new getset val", name))); 709 | }; 710 | 711 | tests.MGET = function () { 712 | var name = "MGET"; 713 | client.mset(["mget keys 1", "mget val 1", "mget keys 2", "mget val 2", "mget keys 3", "mget val 3"], require_string("OK", name)); 714 | client.MGET("mget keys 1", "mget keys 2", "mget keys 3", function (err, results) { 715 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 716 | assert.strictEqual(3, results.length, name); 717 | assert.strictEqual("mget val 1", results[0].toString(), name); 718 | assert.strictEqual("mget val 2", results[1].toString(), name); 719 | assert.strictEqual("mget val 3", results[2].toString(), name); 720 | }); 721 | client.MGET(["mget keys 1", "mget keys 2", "mget keys 3"], function (err, results) { 722 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 723 | assert.strictEqual(3, results.length, name); 724 | assert.strictEqual("mget val 1", results[0].toString(), name); 725 | assert.strictEqual("mget val 2", results[1].toString(), name); 726 | assert.strictEqual("mget val 3", results[2].toString(), name); 727 | }); 728 | client.MGET(["mget keys 1", "some random shit", "mget keys 2", "mget keys 3"], function (err, results) { 729 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 730 | assert.strictEqual(4, results.length, name); 731 | assert.strictEqual("mget val 1", results[0].toString(), name); 732 | assert.strictEqual(null, results[1], name); 733 | assert.strictEqual("mget val 2", results[2].toString(), name); 734 | assert.strictEqual("mget val 3", results[3].toString(), name); 735 | next(name); 736 | }); 737 | }; 738 | 739 | tests.SETNX = function () { 740 | var name = "SETNX"; 741 | client.set(["setnx key", "setnx value"], require_string("OK", name)); 742 | client.SETNX(["setnx key", "new setnx value"], require_number(0, name)); 743 | client.del(["setnx key"], require_number(1, name)); 744 | client.exists(["setnx key"], require_number(0, name)); 745 | client.SETNX(["setnx key", "new setnx value"], require_number(1, name)); 746 | client.exists(["setnx key"], last(name, require_number(1, name))); 747 | }; 748 | 749 | tests.SETEX = function () { 750 | var name = "SETEX"; 751 | client.SETEX(["setex key", "100", "setex val"], require_string("OK", name)); 752 | client.exists(["setex key"], require_number(1, name)); 753 | client.ttl(["setex key"], last(name, require_number_pos(name))); 754 | }; 755 | 756 | tests.MSETNX = function () { 757 | var name = "MSETNX"; 758 | client.mset(["mset1", "val1", "mset2", "val2", "mset3", "val3"], require_string("OK", name)); 759 | client.MSETNX(["mset3", "val3", "mset4", "val4"], require_number(0, name)); 760 | client.del(["mset3"], require_number(1, name)); 761 | client.MSETNX(["mset3", "val3", "mset4", "val4"], require_number(1, name)); 762 | client.exists(["mset3"], require_number(1, name)); 763 | client.exists(["mset4"], last(name, require_number(1, name))); 764 | }; 765 | 766 | tests.HGETALL = function () { 767 | var name = "HGETALL"; 768 | client.hmset(["hosts", "mjr", "1", "another", "23", "home", "1234"], require_string("OK", name)); 769 | client.HGETALL(["hosts"], function (err, obj) { 770 | assert.strictEqual(null, err, name + " result sent back unexpected error: " + err); 771 | assert.strictEqual(3, Object.keys(obj).length, name); 772 | assert.strictEqual("1", obj.mjr.toString(), name); 773 | assert.strictEqual("23", obj.another.toString(), name); 774 | assert.strictEqual("1234", obj.home.toString(), name); 775 | next(name); 776 | }); 777 | }; 778 | 779 | tests.HGETALL_NULL = function () { 780 | var name = "HGETALL_NULL"; 781 | 782 | client.hgetall("missing", function (err, obj) { 783 | assert.strictEqual(null, err); 784 | assert.strictEqual(null, obj); 785 | next(name); 786 | }); 787 | }; 788 | 789 | tests.UTF8 = function () { 790 | var name = "UTF8", 791 | utf8_sample = "ಠ_ಠ"; 792 | 793 | client.set(["utf8test", utf8_sample], require_string("OK", name)); 794 | client.get(["utf8test"], function (err, obj) { 795 | assert.strictEqual(null, err); 796 | assert.strictEqual(utf8_sample, obj); 797 | next(name); 798 | }); 799 | }; 800 | 801 | // Set tests were adapted from Brian Hammond's redis-node-client.js, which has a comprehensive test suite 802 | 803 | tests.SADD = function () { 804 | var name = "SADD"; 805 | 806 | client.del('set0'); 807 | client.sadd('set0', 'member0', require_number(1, name)); 808 | client.sadd('set0', 'member0', last(name, require_number(0, name))); 809 | }; 810 | 811 | tests.SADD2 = function () { 812 | var name = "SADD2"; 813 | 814 | client.del("set0"); 815 | client.sadd("set0", ["member0", "member1", "member2"], require_number(3, name)); 816 | client.smembers("set0", function (err, res) { 817 | assert.strictEqual(res.length, 3); 818 | res.sort(); 819 | assert.strictEqual(res[0], "member0"); 820 | assert.strictEqual(res[1], "member1"); 821 | assert.strictEqual(res[2], "member2"); 822 | next(name); 823 | }); 824 | }; 825 | 826 | tests.SISMEMBER = function () { 827 | var name = "SISMEMBER"; 828 | 829 | client.del('set0'); 830 | client.sadd('set0', 'member0', require_number(1, name)); 831 | client.sismember('set0', 'member0', require_number(1, name)); 832 | client.sismember('set0', 'member1', last(name, require_number(0, name))); 833 | }; 834 | 835 | tests.SCARD = function () { 836 | var name = "SCARD"; 837 | 838 | client.del('set0'); 839 | client.sadd('set0', 'member0', require_number(1, name)); 840 | client.scard('set0', require_number(1, name)); 841 | client.sadd('set0', 'member1', require_number(1, name)); 842 | client.scard('set0', last(name, require_number(2, name))); 843 | }; 844 | 845 | tests.SREM = function () { 846 | var name = "SREM"; 847 | 848 | client.del('set0'); 849 | client.sadd('set0', 'member0', require_number(1, name)); 850 | client.srem('set0', 'foobar', require_number(0, name)); 851 | client.srem('set0', 'member0', require_number(1, name)); 852 | client.scard('set0', last(name, require_number(0, name))); 853 | }; 854 | 855 | tests.SPOP = function () { 856 | var name = "SPOP"; 857 | 858 | client.del('zzz'); 859 | client.sadd('zzz', 'member0', require_number(1, name)); 860 | client.scard('zzz', require_number(1, name)); 861 | 862 | client.spop('zzz', function (err, value) { 863 | if (err) { 864 | assert.fail(err); 865 | } 866 | assert.equal(value, 'member0', name); 867 | }); 868 | 869 | client.scard('zzz', last(name, require_number(0, name))); 870 | }; 871 | 872 | tests.SDIFF = function () { 873 | var name = "SDIFF"; 874 | 875 | client.del('foo'); 876 | client.sadd('foo', 'x', require_number(1, name)); 877 | client.sadd('foo', 'a', require_number(1, name)); 878 | client.sadd('foo', 'b', require_number(1, name)); 879 | client.sadd('foo', 'c', require_number(1, name)); 880 | 881 | client.sadd('bar', 'c', require_number(1, name)); 882 | 883 | client.sadd('baz', 'a', require_number(1, name)); 884 | client.sadd('baz', 'd', require_number(1, name)); 885 | 886 | client.sdiff('foo', 'bar', 'baz', function (err, values) { 887 | if (err) { 888 | assert.fail(err, name); 889 | } 890 | values.sort(); 891 | assert.equal(values.length, 2, name); 892 | assert.equal(values[0], 'b', name); 893 | assert.equal(values[1], 'x', name); 894 | next(name); 895 | }); 896 | }; 897 | 898 | tests.SDIFFSTORE = function () { 899 | var name = "SDIFFSTORE"; 900 | 901 | client.del('foo'); 902 | client.del('bar'); 903 | client.del('baz'); 904 | client.del('quux'); 905 | 906 | client.sadd('foo', 'x', require_number(1, name)); 907 | client.sadd('foo', 'a', require_number(1, name)); 908 | client.sadd('foo', 'b', require_number(1, name)); 909 | client.sadd('foo', 'c', require_number(1, name)); 910 | 911 | client.sadd('bar', 'c', require_number(1, name)); 912 | 913 | client.sadd('baz', 'a', require_number(1, name)); 914 | client.sadd('baz', 'd', require_number(1, name)); 915 | 916 | // NB: SDIFFSTORE returns the number of elements in the dstkey 917 | 918 | client.sdiffstore('quux', 'foo', 'bar', 'baz', require_number(2, name)); 919 | 920 | client.smembers('quux', function (err, values) { 921 | if (err) { 922 | assert.fail(err, name); 923 | } 924 | var members = buffers_to_strings(values).sort(); 925 | 926 | assert.deepEqual(members, [ 'b', 'x' ], name); 927 | next(name); 928 | }); 929 | }; 930 | 931 | tests.SMEMBERS = function () { 932 | var name = "SMEMBERS"; 933 | 934 | client.del('foo'); 935 | client.sadd('foo', 'x', require_number(1, name)); 936 | 937 | client.smembers('foo', function (err, members) { 938 | if (err) { 939 | assert.fail(err, name); 940 | } 941 | assert.deepEqual(buffers_to_strings(members), [ 'x' ], name); 942 | }); 943 | 944 | client.sadd('foo', 'y', require_number(1, name)); 945 | 946 | client.smembers('foo', function (err, values) { 947 | if (err) { 948 | assert.fail(err, name); 949 | } 950 | assert.equal(values.length, 2, name); 951 | var members = buffers_to_strings(values).sort(); 952 | 953 | assert.deepEqual(members, [ 'x', 'y' ], name); 954 | next(name); 955 | }); 956 | }; 957 | 958 | tests.SMOVE = function () { 959 | var name = "SMOVE"; 960 | 961 | client.del('foo'); 962 | client.del('bar'); 963 | 964 | client.sadd('foo', 'x', require_number(1, name)); 965 | client.smove('foo', 'bar', 'x', require_number(1, name)); 966 | client.sismember('foo', 'x', require_number(0, name)); 967 | client.sismember('bar', 'x', require_number(1, name)); 968 | client.smove('foo', 'bar', 'x', last(name, require_number(0, name))); 969 | }; 970 | 971 | tests.SINTER = function () { 972 | var name = "SINTER"; 973 | 974 | client.del('sa'); 975 | client.del('sb'); 976 | client.del('sc'); 977 | 978 | client.sadd('sa', 'a', require_number(1, name)); 979 | client.sadd('sa', 'b', require_number(1, name)); 980 | client.sadd('sa', 'c', require_number(1, name)); 981 | 982 | client.sadd('sb', 'b', require_number(1, name)); 983 | client.sadd('sb', 'c', require_number(1, name)); 984 | client.sadd('sb', 'd', require_number(1, name)); 985 | 986 | client.sadd('sc', 'c', require_number(1, name)); 987 | client.sadd('sc', 'd', require_number(1, name)); 988 | client.sadd('sc', 'e', require_number(1, name)); 989 | 990 | client.sinter('sa', 'sb', function (err, intersection) { 991 | if (err) { 992 | assert.fail(err, name); 993 | } 994 | assert.equal(intersection.length, 2, name); 995 | assert.deepEqual(buffers_to_strings(intersection).sort(), [ 'b', 'c' ], name); 996 | }); 997 | 998 | client.sinter('sb', 'sc', function (err, intersection) { 999 | if (err) { 1000 | assert.fail(err, name); 1001 | } 1002 | assert.equal(intersection.length, 2, name); 1003 | assert.deepEqual(buffers_to_strings(intersection).sort(), [ 'c', 'd' ], name); 1004 | }); 1005 | 1006 | client.sinter('sa', 'sc', function (err, intersection) { 1007 | if (err) { 1008 | assert.fail(err, name); 1009 | } 1010 | assert.equal(intersection.length, 1, name); 1011 | assert.equal(intersection[0], 'c', name); 1012 | }); 1013 | 1014 | // 3-way 1015 | 1016 | client.sinter('sa', 'sb', 'sc', function (err, intersection) { 1017 | if (err) { 1018 | assert.fail(err, name); 1019 | } 1020 | assert.equal(intersection.length, 1, name); 1021 | assert.equal(intersection[0], 'c', name); 1022 | next(name); 1023 | }); 1024 | }; 1025 | 1026 | tests.SINTERSTORE = function () { 1027 | var name = "SINTERSTORE"; 1028 | 1029 | client.del('sa'); 1030 | client.del('sb'); 1031 | client.del('sc'); 1032 | client.del('foo'); 1033 | 1034 | client.sadd('sa', 'a', require_number(1, name)); 1035 | client.sadd('sa', 'b', require_number(1, name)); 1036 | client.sadd('sa', 'c', require_number(1, name)); 1037 | 1038 | client.sadd('sb', 'b', require_number(1, name)); 1039 | client.sadd('sb', 'c', require_number(1, name)); 1040 | client.sadd('sb', 'd', require_number(1, name)); 1041 | 1042 | client.sadd('sc', 'c', require_number(1, name)); 1043 | client.sadd('sc', 'd', require_number(1, name)); 1044 | client.sadd('sc', 'e', require_number(1, name)); 1045 | 1046 | client.sinterstore('foo', 'sa', 'sb', 'sc', require_number(1, name)); 1047 | 1048 | client.smembers('foo', function (err, members) { 1049 | if (err) { 1050 | assert.fail(err, name); 1051 | } 1052 | assert.deepEqual(buffers_to_strings(members), [ 'c' ], name); 1053 | next(name); 1054 | }); 1055 | }; 1056 | 1057 | tests.SUNION = function () { 1058 | var name = "SUNION"; 1059 | 1060 | client.del('sa'); 1061 | client.del('sb'); 1062 | client.del('sc'); 1063 | 1064 | client.sadd('sa', 'a', require_number(1, name)); 1065 | client.sadd('sa', 'b', require_number(1, name)); 1066 | client.sadd('sa', 'c', require_number(1, name)); 1067 | 1068 | client.sadd('sb', 'b', require_number(1, name)); 1069 | client.sadd('sb', 'c', require_number(1, name)); 1070 | client.sadd('sb', 'd', require_number(1, name)); 1071 | 1072 | client.sadd('sc', 'c', require_number(1, name)); 1073 | client.sadd('sc', 'd', require_number(1, name)); 1074 | client.sadd('sc', 'e', require_number(1, name)); 1075 | 1076 | client.sunion('sa', 'sb', 'sc', function (err, union) { 1077 | if (err) { 1078 | assert.fail(err, name); 1079 | } 1080 | assert.deepEqual(buffers_to_strings(union).sort(), ['a', 'b', 'c', 'd', 'e'], name); 1081 | next(name); 1082 | }); 1083 | }; 1084 | 1085 | tests.SUNIONSTORE = function () { 1086 | var name = "SUNIONSTORE"; 1087 | 1088 | client.del('sa'); 1089 | client.del('sb'); 1090 | client.del('sc'); 1091 | client.del('foo'); 1092 | 1093 | client.sadd('sa', 'a', require_number(1, name)); 1094 | client.sadd('sa', 'b', require_number(1, name)); 1095 | client.sadd('sa', 'c', require_number(1, name)); 1096 | 1097 | client.sadd('sb', 'b', require_number(1, name)); 1098 | client.sadd('sb', 'c', require_number(1, name)); 1099 | client.sadd('sb', 'd', require_number(1, name)); 1100 | 1101 | client.sadd('sc', 'c', require_number(1, name)); 1102 | client.sadd('sc', 'd', require_number(1, name)); 1103 | client.sadd('sc', 'e', require_number(1, name)); 1104 | 1105 | client.sunionstore('foo', 'sa', 'sb', 'sc', function (err, cardinality) { 1106 | if (err) { 1107 | assert.fail(err, name); 1108 | } 1109 | assert.equal(cardinality, 5, name); 1110 | }); 1111 | 1112 | client.smembers('foo', function (err, members) { 1113 | if (err) { 1114 | assert.fail(err, name); 1115 | } 1116 | assert.equal(members.length, 5, name); 1117 | assert.deepEqual(buffers_to_strings(members).sort(), ['a', 'b', 'c', 'd', 'e'], name); 1118 | next(name); 1119 | }); 1120 | }; 1121 | 1122 | // SORT test adapted from Brian Hammond's redis-node-client.js, which has a comprehensive test suite 1123 | 1124 | tests.SORT = function () { 1125 | var name = "SORT"; 1126 | 1127 | client.del('y'); 1128 | client.del('x'); 1129 | 1130 | client.rpush('y', 'd', require_number(1, name)); 1131 | client.rpush('y', 'b', require_number(2, name)); 1132 | client.rpush('y', 'a', require_number(3, name)); 1133 | client.rpush('y', 'c', require_number(4, name)); 1134 | 1135 | client.rpush('x', '3', require_number(1, name)); 1136 | client.rpush('x', '9', require_number(2, name)); 1137 | client.rpush('x', '2', require_number(3, name)); 1138 | client.rpush('x', '4', require_number(4, name)); 1139 | 1140 | client.set('w3', '4', require_string("OK", name)); 1141 | client.set('w9', '5', require_string("OK", name)); 1142 | client.set('w2', '12', require_string("OK", name)); 1143 | client.set('w4', '6', require_string("OK", name)); 1144 | 1145 | client.set('o2', 'buz', require_string("OK", name)); 1146 | client.set('o3', 'foo', require_string("OK", name)); 1147 | client.set('o4', 'baz', require_string("OK", name)); 1148 | client.set('o9', 'bar', require_string("OK", name)); 1149 | 1150 | client.set('p2', 'qux', require_string("OK", name)); 1151 | client.set('p3', 'bux', require_string("OK", name)); 1152 | client.set('p4', 'lux', require_string("OK", name)); 1153 | client.set('p9', 'tux', require_string("OK", name)); 1154 | 1155 | // Now the data has been setup, we can test. 1156 | 1157 | // But first, test basic sorting. 1158 | 1159 | // y = [ d b a c ] 1160 | // sort y ascending = [ a b c d ] 1161 | // sort y descending = [ d c b a ] 1162 | 1163 | client.sort('y', 'asc', 'alpha', function (err, sorted) { 1164 | if (err) { 1165 | assert.fail(err, name); 1166 | } 1167 | assert.deepEqual(buffers_to_strings(sorted), ['a', 'b', 'c', 'd'], name); 1168 | }); 1169 | 1170 | client.sort('y', 'desc', 'alpha', function (err, sorted) { 1171 | if (err) { 1172 | assert.fail(err, name); 1173 | } 1174 | assert.deepEqual(buffers_to_strings(sorted), ['d', 'c', 'b', 'a'], name); 1175 | }); 1176 | 1177 | // Now try sorting numbers in a list. 1178 | // x = [ 3, 9, 2, 4 ] 1179 | 1180 | client.sort('x', 'asc', function (err, sorted) { 1181 | if (err) { 1182 | assert.fail(err, name); 1183 | } 1184 | assert.deepEqual(buffers_to_strings(sorted), [2, 3, 4, 9], name); 1185 | }); 1186 | 1187 | client.sort('x', 'desc', function (err, sorted) { 1188 | if (err) { 1189 | assert.fail(err, name); 1190 | } 1191 | assert.deepEqual(buffers_to_strings(sorted), [9, 4, 3, 2], name); 1192 | }); 1193 | 1194 | // Try sorting with a 'by' pattern. 1195 | 1196 | client.sort('x', 'by', 'w*', 'asc', function (err, sorted) { 1197 | if (err) { 1198 | assert.fail(err, name); 1199 | } 1200 | assert.deepEqual(buffers_to_strings(sorted), [3, 9, 4, 2], name); 1201 | }); 1202 | 1203 | // Try sorting with a 'by' pattern and 1 'get' pattern. 1204 | 1205 | client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', function (err, sorted) { 1206 | if (err) { 1207 | assert.fail(err, name); 1208 | } 1209 | assert.deepEqual(buffers_to_strings(sorted), ['foo', 'bar', 'baz', 'buz'], name); 1210 | }); 1211 | 1212 | // Try sorting with a 'by' pattern and 2 'get' patterns. 1213 | 1214 | client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', function (err, sorted) { 1215 | if (err) { 1216 | assert.fail(err, name); 1217 | } 1218 | assert.deepEqual(buffers_to_strings(sorted), ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux'], name); 1219 | }); 1220 | 1221 | // Try sorting with a 'by' pattern and 2 'get' patterns. 1222 | // Instead of getting back the sorted set/list, store the values to a list. 1223 | // Then check that the values are there in the expected order. 1224 | 1225 | client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', 'store', 'bacon', function (err) { 1226 | if (err) { 1227 | assert.fail(err, name); 1228 | } 1229 | }); 1230 | 1231 | client.lrange('bacon', 0, -1, function (err, values) { 1232 | if (err) { 1233 | assert.fail(err, name); 1234 | } 1235 | assert.deepEqual(buffers_to_strings(values), ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux'], name); 1236 | next(name); 1237 | }); 1238 | 1239 | // TODO - sort by hash value 1240 | }; 1241 | 1242 | tests.MONITOR = function () { 1243 | var name = "MONITOR", responses = [], monitor_client; 1244 | 1245 | if (client.server_info.versions[0] == 2 && client.server_info.versions[1] <= 4) { 1246 | monitor_client = redis.createClient(); 1247 | monitor_client.monitor(function (err, res) { 1248 | client.mget("some", "keys", "foo", "bar"); 1249 | client.set("json", JSON.stringify({ 1250 | foo: "123", 1251 | bar: "sdflkdfsjk", 1252 | another: false 1253 | })); 1254 | }); 1255 | monitor_client.on("monitor", function (time, args) { 1256 | responses.push(args); 1257 | if (responses.length === 3) { 1258 | assert.strictEqual(1, responses[0].length); 1259 | assert.strictEqual("monitor", responses[0][0]); 1260 | assert.strictEqual(5, responses[1].length); 1261 | assert.strictEqual("mget", responses[1][0]); 1262 | assert.strictEqual("some", responses[1][1]); 1263 | assert.strictEqual("keys", responses[1][2]); 1264 | assert.strictEqual("foo", responses[1][3]); 1265 | assert.strictEqual("bar", responses[1][4]); 1266 | assert.strictEqual(3, responses[2].length); 1267 | assert.strictEqual("set", responses[2][0]); 1268 | assert.strictEqual("json", responses[2][1]); 1269 | assert.strictEqual('{"foo":"123","bar":"sdflkdfsjk","another":false}', responses[2][2]); 1270 | monitor_client.quit(function (err, res) { 1271 | next(name); 1272 | }); 1273 | } 1274 | }); 1275 | } 1276 | else { 1277 | console.log("Skipping " + name + " because server version seems to choke on it."); 1278 | next(name); 1279 | } 1280 | }; 1281 | 1282 | tests.BLPOP = function () { 1283 | var name = "BLPOP"; 1284 | 1285 | client.rpush("blocking list", "initial value", function (err, res) { 1286 | client2.BLPOP("blocking list", 0, function (err, res) { 1287 | assert.strictEqual("blocking list", res[0].toString()); 1288 | assert.strictEqual("initial value", res[1].toString()); 1289 | 1290 | client.rpush("blocking list", "wait for this value"); 1291 | }); 1292 | client2.BLPOP("blocking list", 0, function (err, res) { 1293 | assert.strictEqual("blocking list", res[0].toString()); 1294 | assert.strictEqual("wait for this value", res[1].toString()); 1295 | next(name); 1296 | }); 1297 | }); 1298 | }; 1299 | 1300 | tests.BLPOP_TIMEOUT = function () { 1301 | var name = "BLPOP_TIMEOUT"; 1302 | 1303 | // try to BLPOP the list again, which should be empty. This should timeout and return null. 1304 | client2.BLPOP("blocking list", 1, function (err, res) { 1305 | if (err) { 1306 | throw err; 1307 | } 1308 | 1309 | assert.strictEqual(res, null); 1310 | next(name); 1311 | }); 1312 | }; 1313 | 1314 | tests.EXPIRE = function () { 1315 | var name = "EXPIRE"; 1316 | client.set(['expiry key', 'bar'], require_string("OK", name)); 1317 | client.EXPIRE(["expiry key", "1"], require_number_pos(name)); 1318 | setTimeout(function () { 1319 | client.exists(["expiry key"], last(name, require_number(0, name))); 1320 | }, 2000); 1321 | }; 1322 | 1323 | tests.TTL = function () { 1324 | var name = "TTL"; 1325 | client.set(["ttl key", "ttl val"], require_string("OK", name)); 1326 | client.expire(["ttl key", "100"], require_number_pos(name)); 1327 | setTimeout(function () { 1328 | client.TTL(["ttl key"], last(name, require_number_pos(0, name))); 1329 | }, 500); 1330 | }; 1331 | 1332 | tests.OPTIONAL_CALLBACK = function () { 1333 | var name = "OPTIONAL_CALLBACK"; 1334 | client.del("op_cb1"); 1335 | client.set("op_cb1", "x"); 1336 | client.get("op_cb1", last(name, require_string("x", name))); 1337 | }; 1338 | 1339 | tests.OPTIONAL_CALLBACK_UNDEFINED = function () { 1340 | var name = "OPTIONAL_CALLBACK_UNDEFINED"; 1341 | client.del("op_cb2"); 1342 | client.set("op_cb2", "y", undefined); 1343 | client.get("op_cb2", last(name, require_string("y", name))); 1344 | }; 1345 | 1346 | // TODO - need a better way to test auth, maybe auto-config a local Redis server or something. 1347 | // Yes, this is the real password. Please be nice, thanks. 1348 | /* disabled because this is not runnable on travis - carlos8f 1349 | tests.auth = function () { 1350 | var name = "AUTH", client4, ready_count = 0; 1351 | 1352 | client4 = redis.createClient(9006, "filefish.redistogo.com"); 1353 | client4.auth("664b1b6aaf134e1ec281945a8de702a9", function (err, res) { 1354 | assert.strictEqual(null, err, name); 1355 | assert.strictEqual("OK", res.toString(), name); 1356 | }); 1357 | 1358 | // test auth, then kill the connection so it'll auto-reconnect and auto-re-auth 1359 | client4.on("ready", function () { 1360 | ready_count++; 1361 | if (ready_count === 1) { 1362 | client4.stream.destroy(); 1363 | } else { 1364 | client4.quit(function (err, res) { 1365 | next(name); 1366 | }); 1367 | } 1368 | }); 1369 | };*/ 1370 | 1371 | all_tests = Object.keys(tests); 1372 | all_start = new Date(); 1373 | test_count = 0; 1374 | 1375 | run_next_test = function run_next_test() { 1376 | var test_name = all_tests.shift(); 1377 | if (typeof tests[test_name] === "function") { 1378 | util.print('- \x1b[1m' + test_name.toLowerCase() + '\x1b[0m:'); 1379 | cur_start = new Date(); 1380 | test_count += 1; 1381 | tests[test_name](); 1382 | } else { 1383 | console.log('\n completed \x1b[32m%d\x1b[0m tests in \x1b[33m%d\x1b[0m ms\n', test_count, new Date() - all_start); 1384 | client.quit(); 1385 | client2.quit(); 1386 | } 1387 | }; 1388 | 1389 | client.once("ready", function start_tests() { 1390 | console.log("Connected to " + client.host + ":" + client.port + ", Redis server version " + client.server_info.redis_version + "\n"); 1391 | console.log("Using reply parser " + client.reply_parser.name); 1392 | 1393 | run_next_test(); 1394 | 1395 | connected = true; 1396 | }); 1397 | 1398 | client.on('end', function () { 1399 | ended = true; 1400 | }); 1401 | 1402 | // Exit immediately on connection failure, which triggers "exit", below, which fails the test 1403 | client.on("error", function (err) { 1404 | console.error("client: " + err.stack); 1405 | process.exit(); 1406 | }); 1407 | client2.on("error", function (err) { 1408 | console.error("client2: " + err.stack); 1409 | process.exit(); 1410 | }); 1411 | client3.on("error", function (err) { 1412 | console.error("client3: " + err.stack); 1413 | process.exit(); 1414 | }); 1415 | client.on("reconnecting", function (params) { 1416 | console.log("reconnecting: " + util.inspect(params)); 1417 | }); 1418 | 1419 | process.on('uncaughtException', function (err) { 1420 | console.error("Uncaught exception: " + err.stack); 1421 | process.exit(1); 1422 | }); 1423 | 1424 | process.on('exit', function (code) { 1425 | assert.equal(true, connected); 1426 | assert.equal(true, ended); 1427 | }); 1428 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var nodes = [6380, 6381, 6382]; 2 | console.log('Note: test requires redis daemons running locally on ports 6380, 6381, and 6382!'); 3 | 4 | /*global require console setTimeout process Buffer */ 5 | var redis = require("./index"), 6 | client = redis.createClient(nodes), 7 | client2 = redis.createClient(nodes), 8 | client3 = redis.createClient(nodes), 9 | assert = require("assert"), 10 | util = require("util"), 11 | test_db_num = 15, // this DB will be flushed and used for testing 12 | tests = {}, 13 | connected = false, 14 | ended = false, 15 | next, cur_start, run_next_test, all_tests, all_start, test_count; 16 | 17 | // Set this to truthy to see the wire protocol and other debugging info 18 | redis.debug_mode = process.argv[2]; 19 | 20 | function buffers_to_strings(arr) { 21 | return arr.map(function (val) { 22 | return val.toString(); 23 | }); 24 | } 25 | 26 | function require_number(expected, label) { 27 | return function (err, results) { 28 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 29 | assert.strictEqual(expected, results, label + " " + expected + " !== " + results); 30 | assert.strictEqual(typeof results, "number", label); 31 | return true; 32 | }; 33 | } 34 | 35 | function require_number_any(label) { 36 | return function (err, results) { 37 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 38 | assert.strictEqual(typeof results, "number", label + " " + results + " is not a number"); 39 | return true; 40 | }; 41 | } 42 | 43 | function require_number_pos(label) { 44 | return function (err, results) { 45 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 46 | assert.strictEqual(true, (results > 0), label + " " + results + " is not a positive number"); 47 | return true; 48 | }; 49 | } 50 | 51 | function require_string(str, label) { 52 | return function (err, results) { 53 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 54 | assert.equal(str, results, label + " " + str + " does not match " + results); 55 | return true; 56 | }; 57 | } 58 | 59 | function require_null(label) { 60 | return function (err, results) { 61 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 62 | assert.strictEqual(null, results, label + ": " + results + " is not null"); 63 | return true; 64 | }; 65 | } 66 | 67 | function require_error(label) { 68 | return function (err, results) { 69 | assert.notEqual(err, null, label + " err is null, but an error is expected here."); 70 | return true; 71 | }; 72 | } 73 | 74 | function is_empty_array(obj) { 75 | return Array.isArray(obj) && obj.length === 0; 76 | } 77 | 78 | function last(name, fn) { 79 | return function (err, results) { 80 | fn(err, results); 81 | next(name); 82 | }; 83 | } 84 | 85 | next = function next(name) { 86 | console.log(" \x1b[33m" + (Date.now() - cur_start) + "\x1b[0m ms"); 87 | run_next_test(); 88 | }; 89 | 90 | // Tests are run in the order they are defined. So FLUSHDB should be stay first. 91 | 92 | tests.FLUSHDB = function () { 93 | var name = "FLUSHDB"; 94 | client.select(test_db_num, require_string("OK", name)); 95 | client2.select(test_db_num, require_string("OK", name)); 96 | client3.select(test_db_num, require_string("OK", name)); 97 | client.mset("flush keys 1", "flush val 1", "flush keys 2", "flush val 2", require_string("OK", name)); 98 | client.FLUSHDB(require_string("OK", name)); 99 | client.dbsize(last(name, require_number(0, name))); 100 | }; 101 | 102 | tests.MULTI_1 = function () { 103 | var name = "MULTI_1", multi1, multi2; 104 | 105 | // Provoke an error at queue time 106 | multi1 = client.multi(); 107 | multi1.mset("multifoo", "10", "multibar", "20", require_string("OK", name)); 108 | // newer server version cancels transaction with EXECABORT, so disable this test. 109 | //multi1.set("foo2", require_error(name)); 110 | multi1.incr("multifoo", require_number(11, name)); 111 | multi1.incr("multibar", require_number(21, name)); 112 | multi1.exec(); 113 | 114 | // Confirm that the previous command, while containing an error, still worked. 115 | multi2 = client.multi(); 116 | multi2.incr("multibar", require_number(22, name)); 117 | multi2.incr("multifoo", require_number(12, name)); 118 | multi2.exec(function (err, replies) { 119 | assert.strictEqual(22, replies[0]); 120 | assert.strictEqual(12, replies[1]); 121 | next(name); 122 | }); 123 | }; 124 | 125 | tests.MULTI_2 = function () { 126 | var name = "MULTI_2"; 127 | 128 | // test nested multi-bulk replies 129 | client.multi([ 130 | ["mget", "multifoo", "multibar", function (err, res) { 131 | assert.strictEqual(2, res.length, name); 132 | assert.strictEqual("12", res[0].toString(), name); 133 | assert.strictEqual("22", res[1].toString(), name); 134 | }], 135 | // newer server version cancels transaction with EXECABORT, so disable this test. 136 | //["set", "foo2", require_error(name)], 137 | ["incr", "multifoo", require_number(13, name)], 138 | ["incr", "multibar", require_number(23, name)] 139 | ]).exec(function (err, replies) { 140 | assert.strictEqual(2, replies[0].length, name); 141 | assert.strictEqual("12", replies[0][0].toString(), name); 142 | assert.strictEqual("22", replies[0][1].toString(), name); 143 | 144 | assert.strictEqual("13", replies[1].toString()); 145 | assert.strictEqual("23", replies[2].toString()); 146 | next(name); 147 | }); 148 | }; 149 | 150 | tests.MULTI_3 = function () { 151 | var name = "MULTI_3"; 152 | 153 | client.sadd("some set", "mem 1"); 154 | client.sadd("some set", "mem 2"); 155 | client.sadd("some set", "mem 3"); 156 | client.sadd("some set", "mem 4"); 157 | 158 | // make sure empty mb reply works 159 | client.del("some missing set"); 160 | client.smembers("some missing set", function (err, reply) { 161 | // make sure empty mb reply works 162 | assert.strictEqual(true, is_empty_array(reply), name); 163 | }); 164 | 165 | // test nested multi-bulk replies with empty mb elements. 166 | client.multi([ 167 | ["smembers", "some set"], 168 | ["del", "some set"], 169 | ["smembers", "some set"] 170 | ]) 171 | .scard("some set") 172 | .exec(function (err, replies) { 173 | assert.strictEqual(true, is_empty_array(replies[2]), name); 174 | next(name); 175 | }); 176 | }; 177 | 178 | tests.MULTI_4 = function () { 179 | var name = "MULTI_4"; 180 | 181 | client.multi() 182 | .mset('some', '10', 'keys', '20') 183 | .incr('some') 184 | .incr('keys') 185 | .mget('some', 'keys') 186 | .exec(function (err, replies) { 187 | assert.strictEqual(null, err); 188 | assert.equal('OK', replies[0]); 189 | assert.equal(11, replies[1]); 190 | assert.equal(21, replies[2]); 191 | assert.equal(11, replies[3][0].toString()); 192 | assert.equal(21, replies[3][1].toString()); 193 | next(name); 194 | }); 195 | }; 196 | 197 | tests.MULTI_5 = function () { 198 | var name = "MULTI_5"; 199 | 200 | // test nested multi-bulk replies with nulls. 201 | client.multi([ 202 | ["mget", ["multifoo", "some", "random value", "keys"]], 203 | ["incr", "multifoo"] 204 | ]) 205 | .exec(function (err, replies) { 206 | assert.strictEqual(replies.length, 2, name); 207 | assert.strictEqual(replies[0].length, 4, name); 208 | next(name); 209 | }); 210 | }; 211 | 212 | tests.MULTI_6 = function () { 213 | var name = "MULTI_6"; 214 | 215 | client.multi() 216 | .hmset("multihash", "a", "foo", "b", 1) 217 | .hmset("multihash", { 218 | extra: "fancy", 219 | things: "here" 220 | }) 221 | .hgetall("multihash") 222 | .exec(function (err, replies) { 223 | assert.strictEqual(null, err); 224 | assert.equal("OK", replies[0]); 225 | assert.equal(Object.keys(replies[2]).length, 4); 226 | assert.equal("foo", replies[2].a); 227 | assert.equal("1", replies[2].b); 228 | assert.equal("fancy", replies[2].extra); 229 | assert.equal("here", replies[2].things); 230 | next(name); 231 | }); 232 | }; 233 | 234 | tests.EVAL_1 = function () { 235 | var name = "EVAL_1"; 236 | 237 | if (client.server_info.versions[0] >= 2 && client.server_info.versions[1] >= 9) { 238 | // test {EVAL - Lua integer -> Redis protocol type conversion} 239 | client.eval("return 100.5", 0, require_number(100, name)); 240 | // test {EVAL - Lua string -> Redis protocol type conversion} 241 | client.eval("return 'hello world'", 0, require_string("hello world", name)); 242 | // test {EVAL - Lua true boolean -> Redis protocol type conversion} 243 | client.eval("return true", 0, require_number(1, name)); 244 | // test {EVAL - Lua false boolean -> Redis protocol type conversion} 245 | client.eval("return false", 0, require_null(name)); 246 | // test {EVAL - Lua status code reply -> Redis protocol type conversion} 247 | client.eval("return {ok='fine'}", 0, require_string("fine", name)); 248 | // test {EVAL - Lua error reply -> Redis protocol type conversion} 249 | client.eval("return {err='this is an error'}", 0, require_error(name)); 250 | // test {EVAL - Lua table -> Redis protocol type conversion} 251 | client.eval("return {1,2,3,'ciao',{1,2}}", 0, function (err, res) { 252 | assert.strictEqual(5, res.length, name); 253 | assert.strictEqual(1, res[0], name); 254 | assert.strictEqual(2, res[1], name); 255 | assert.strictEqual(3, res[2], name); 256 | assert.strictEqual("ciao", res[3], name); 257 | assert.strictEqual(2, res[4].length, name); 258 | assert.strictEqual(1, res[4][0], name); 259 | assert.strictEqual(2, res[4][1], name); 260 | }); 261 | // test {EVAL - Are the KEYS and ARGS arrays populated correctly?} 262 | client.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "a", "b", "c", "d", function (err, res) { 263 | assert.strictEqual(4, res.length, name); 264 | assert.strictEqual("a", res[0], name); 265 | assert.strictEqual("b", res[1], name); 266 | assert.strictEqual("c", res[2], name); 267 | assert.strictEqual("d", res[3], name); 268 | }); 269 | // test {EVAL - is Lua able to call Redis API?} 270 | client.set("mykey", "myval"); 271 | client.eval("return redis.call('get','mykey')", 0, require_string("myval", name)); 272 | // test {EVALSHA - Can we call a SHA1 if already defined?} 273 | client.evalsha("9bd632c7d33e571e9f24556ebed26c3479a87129", 0, require_string("myval", name)); 274 | // test {EVALSHA - Do we get an error on non defined SHA1?} 275 | client.evalsha("ffffffffffffffffffffffffffffffffffffffff", 0, require_error(name)); 276 | // test {EVAL - Redis integer -> Lua type conversion} 277 | client.set("x", 0); 278 | client.eval("local foo = redis.call('incr','x')\n" + "return {type(foo),foo}", 0, function (err, res) { 279 | assert.strictEqual(2, res.length, name); 280 | assert.strictEqual("number", res[0], name); 281 | assert.strictEqual(1, res[1], name); 282 | }); 283 | // test {EVAL - Redis bulk -> Lua type conversion} 284 | client.eval("local foo = redis.call('get','mykey'); return {type(foo),foo}", 0, function (err, res) { 285 | assert.strictEqual(2, res.length, name); 286 | assert.strictEqual("string", res[0], name); 287 | assert.strictEqual("myval", res[1], name); 288 | }); 289 | // test {EVAL - Redis multi bulk -> Lua type conversion} 290 | client.del("mylist"); 291 | client.rpush("mylist", "a"); 292 | client.rpush("mylist", "b"); 293 | client.rpush("mylist", "c"); 294 | client.eval("local foo = redis.call('lrange','mylist',0,-1)\n" + "return {type(foo),foo[1],foo[2],foo[3],# foo}", 0, function (err, res) { 295 | assert.strictEqual(5, res.length, name); 296 | assert.strictEqual("table", res[0], name); 297 | assert.strictEqual("a", res[1], name); 298 | assert.strictEqual("b", res[2], name); 299 | assert.strictEqual("c", res[3], name); 300 | assert.strictEqual(3, res[4], name); 301 | }); 302 | // test {EVAL - Redis status reply -> Lua type conversion} 303 | client.eval("local foo = redis.call('set','mykey','myval'); return {type(foo),foo['ok']}", 0, function (err, res) { 304 | assert.strictEqual(2, res.length, name); 305 | assert.strictEqual("table", res[0], name); 306 | assert.strictEqual("OK", res[1], name); 307 | }); 308 | // test {EVAL - Redis error reply -> Lua type conversion} 309 | client.set("mykey", "myval"); 310 | client.eval("local foo = redis.call('incr','mykey'); return {type(foo),foo['err']}", 0, function (err, res) { 311 | assert.strictEqual(2, res.length, name); 312 | assert.strictEqual("table", res[0], name); 313 | assert.strictEqual("ERR value is not an integer or out of range", res[1], name); 314 | }); 315 | // test {EVAL - Redis nil bulk reply -> Lua type conversion} 316 | client.del("mykey"); 317 | client.eval("local foo = redis.call('get','mykey'); return {type(foo),foo == false}", 0, function (err, res) { 318 | assert.strictEqual(2, res.length, name); 319 | assert.strictEqual("boolean", res[0], name); 320 | assert.strictEqual(1, res[1], name); 321 | }); 322 | // test {EVAL - Script can't run more than configured time limit} { 323 | client.config("set", "lua-time-limit", 1); 324 | client.eval("local i = 0; while true do i=i+1 end", 0, last("name", require_error(name))); 325 | } else { 326 | console.log("Skipping " + name + " because server version isn't new enough."); 327 | next(name); 328 | } 329 | }; 330 | 331 | tests.WATCH_MULTI = function () { 332 | var name = 'WATCH_MULTI', multi; 333 | 334 | if (client.server_info.versions[0] >= 2 && client.server_info.versions[1] >= 1) { 335 | client.watch(name); 336 | client.incr(name); 337 | multi = client.multi(); 338 | multi.incr(name); 339 | multi.exec(last(name, require_null(name))); 340 | } else { 341 | console.log("Skipping " + name + " because server version isn't new enough."); 342 | next(name); 343 | } 344 | }; 345 | 346 | tests.detect_buffers = function () { 347 | var name = "detect_buffers", detect_client = redis.createClient(nodes, {detect_buffers: true}); 348 | 349 | detect_client.on("ready", function () { 350 | detect_client.select(test_db_num, require_string("OK", name)); 351 | // single Buffer or String 352 | detect_client.set("string key 1", "string value"); 353 | detect_client.get("string key 1", require_string("string value", name)); 354 | detect_client.get(new Buffer("string key 1"), function (err, reply) { 355 | assert.strictEqual(null, err, name); 356 | assert.strictEqual(true, Buffer.isBuffer(reply), name); 357 | assert.strictEqual("", reply.inspect(), name); 358 | }); 359 | 360 | detect_client.hmset("hash key 2", "key 1", "val 1", "key 2", "val 2"); 361 | // array of Buffers or Strings 362 | detect_client.hmget("hash key 2", "key 1", "key 2", function (err, reply) { 363 | assert.strictEqual(null, err, name); 364 | assert.strictEqual(true, Array.isArray(reply), name); 365 | assert.strictEqual(2, reply.length, name); 366 | assert.strictEqual("val 1", reply[0], name); 367 | assert.strictEqual("val 2", reply[1], name); 368 | }); 369 | detect_client.hmget(new Buffer("hash key 2"), "key 1", "key 2", function (err, reply) { 370 | assert.strictEqual(null, err, name); 371 | assert.strictEqual(true, Array.isArray(reply)); 372 | assert.strictEqual(2, reply.length, name); 373 | assert.strictEqual(true, Buffer.isBuffer(reply[0])); 374 | assert.strictEqual(true, Buffer.isBuffer(reply[1])); 375 | assert.strictEqual("", reply[0].inspect(), name); 376 | assert.strictEqual("", reply[1].inspect(), name); 377 | }); 378 | 379 | // Object of Buffers or Strings 380 | detect_client.hgetall("hash key 2", function (err, reply) { 381 | assert.strictEqual(null, err, name); 382 | assert.strictEqual("object", typeof reply, name); 383 | assert.strictEqual(2, Object.keys(reply).length, name); 384 | assert.strictEqual("val 1", reply["key 1"], name); 385 | assert.strictEqual("val 2", reply["key 2"], name); 386 | }); 387 | detect_client.hgetall(new Buffer("hash key 2"), function (err, reply) { 388 | assert.strictEqual(null, err, name); 389 | assert.strictEqual("object", typeof reply, name); 390 | assert.strictEqual(2, Object.keys(reply).length, name); 391 | assert.strictEqual(true, Buffer.isBuffer(reply["key 1"])); 392 | assert.strictEqual(true, Buffer.isBuffer(reply["key 2"])); 393 | assert.strictEqual("", reply["key 1"].inspect(), name); 394 | assert.strictEqual("", reply["key 2"].inspect(), name); 395 | }); 396 | 397 | detect_client.quit(function (err, res) { 398 | next(name); 399 | }); 400 | }); 401 | }; 402 | 403 | tests.socket_nodelay = function () { 404 | var name = "socket_nodelay", c1, c2, c3, ready_count = 0, quit_count = 0; 405 | 406 | c1 = redis.createClient(nodes, {socket_nodelay: true}); 407 | c2 = redis.createClient(nodes, {socket_nodelay: false}); 408 | c3 = redis.createClient(nodes); 409 | 410 | function quit_check() { 411 | quit_count++; 412 | 413 | if (quit_count === 3) { 414 | next(name); 415 | } 416 | } 417 | 418 | function run() { 419 | assert.strictEqual(true, c1.options.socket_nodelay, name); 420 | assert.strictEqual(false, c2.options.socket_nodelay, name); 421 | assert.strictEqual(true, c3.options.socket_nodelay, name); 422 | 423 | c1.select(test_db_num, require_string("OK", name)); 424 | c1.set(["set key 1", "set val"], require_string("OK", name)); 425 | c1.set(["set key 2", "set val"], require_string("OK", name)); 426 | c1.get(["set key 1"], require_string("set val", name)); 427 | c1.get(["set key 2"], require_string("set val", name)); 428 | 429 | c2.select(test_db_num, require_string("OK", name)); 430 | c2.set(["set key 3", "set val"], require_string("OK", name)); 431 | c2.set(["set key 4", "set val"], require_string("OK", name)); 432 | c2.get(["set key 3"], require_string("set val", name)); 433 | c2.get(["set key 4"], require_string("set val", name)); 434 | 435 | c3.select(test_db_num, require_string("OK", name)); 436 | c3.set(["set key 5", "set val"], require_string("OK", name)); 437 | c3.set(["set key 6", "set val"], require_string("OK", name)); 438 | c3.get(["set key 5"], require_string("set val", name)); 439 | c3.get(["set key 6"], require_string("set val", name)); 440 | 441 | c1.quit(quit_check); 442 | c2.quit(quit_check); 443 | c3.quit(quit_check); 444 | } 445 | 446 | function ready_check() { 447 | ready_count++; 448 | if (ready_count === 3) { 449 | run(); 450 | } 451 | } 452 | 453 | c1.on("ready", ready_check); 454 | c2.on("ready", ready_check); 455 | c3.on("ready", ready_check); 456 | }; 457 | 458 | tests.reconnect = function () { 459 | var name = "reconnect"; 460 | 461 | client.set("recon 1", "one"); 462 | client.set("recon 2", "two", function (err, res) { 463 | // Do not do this in normal programs. This is to simulate the server closing on us. 464 | // For orderly shutdown in normal programs, do client.quit() 465 | client.master.client.stream.destroy(); 466 | }); 467 | 468 | client.on("reconnecting", function on_recon(params) { 469 | client.on("connect", function on_connect() { 470 | client.select(test_db_num, require_string("OK", name)); 471 | client.get("recon 1", require_string("one", name)); 472 | client.get("recon 1", require_string("one", name)); 473 | client.get("recon 2", require_string("two", name)); 474 | client.get("recon 2", require_string("two", name)); 475 | client.removeListener("connect", on_connect); 476 | client.removeListener("reconnecting", on_recon); 477 | next(name); 478 | }); 479 | }); 480 | }; 481 | 482 | tests.HSET = function () { 483 | var key = "test hash", 484 | field1 = new Buffer("0123456789"), 485 | value1 = new Buffer("abcdefghij"), 486 | field2 = new Buffer(0), 487 | value2 = new Buffer(0), 488 | name = "HSET"; 489 | 490 | client.HSET(key, field1, value1, require_number(1, name)); 491 | client.HGET(key, field1, require_string(value1.toString(), name)); 492 | 493 | // Empty value 494 | client.HSET(key, field1, value2, require_number(0, name)); 495 | client.HGET([key, field1], require_string("", name)); 496 | 497 | // Empty key, empty value 498 | client.HSET([key, field2, value1], require_number(1, name)); 499 | client.HSET(key, field2, value2, last(name, require_number(0, name))); 500 | }; 501 | 502 | tests.HMSET_BUFFER_AND_ARRAY = function () { 503 | // Saving a buffer and an array to the same key should not error 504 | var key = "test hash", 505 | field1 = "buffer", 506 | value1 = new Buffer("abcdefghij"), 507 | field2 = "array", 508 | value2 = ["array contents"], 509 | name = "HSET"; 510 | 511 | client.HMSET(key, field1, value1, field2, value2, last(name, require_string("OK", name))); 512 | }; 513 | 514 | // TODO - add test for HMSET with optional callbacks 515 | 516 | tests.HMGET = function () { 517 | var key1 = "test hash 1", key2 = "test hash 2", name = "HMGET"; 518 | 519 | // redis-like hmset syntax 520 | client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value", require_string("OK", name)); 521 | 522 | // fancy hmset syntax 523 | client.HMSET(key2, { 524 | "0123456789": "abcdefghij", 525 | "some manner of key": "a type of value" 526 | }, require_string("OK", name)); 527 | 528 | client.HMGET(key1, "0123456789", "some manner of key", function (err, reply) { 529 | assert.strictEqual("abcdefghij", reply[0].toString(), name); 530 | assert.strictEqual("a type of value", reply[1].toString(), name); 531 | }); 532 | 533 | client.HMGET(key2, "0123456789", "some manner of key", function (err, reply) { 534 | assert.strictEqual("abcdefghij", reply[0].toString(), name); 535 | assert.strictEqual("a type of value", reply[1].toString(), name); 536 | }); 537 | 538 | client.HMGET(key1, ["0123456789"], function (err, reply) { 539 | assert.strictEqual("abcdefghij", reply[0], name); 540 | }); 541 | 542 | client.HMGET(key1, ["0123456789", "some manner of key"], function (err, reply) { 543 | assert.strictEqual("abcdefghij", reply[0], name); 544 | assert.strictEqual("a type of value", reply[1], name); 545 | }); 546 | 547 | client.HMGET(key1, "missing thing", "another missing thing", function (err, reply) { 548 | assert.strictEqual(null, reply[0], name); 549 | assert.strictEqual(null, reply[1], name); 550 | next(name); 551 | }); 552 | }; 553 | 554 | tests.HINCRBY = function () { 555 | var name = "HINCRBY"; 556 | client.hset("hash incr", "value", 10, require_number(1, name)); 557 | client.HINCRBY("hash incr", "value", 1, require_number(11, name)); 558 | client.HINCRBY("hash incr", "value 2", 1, last(name, require_number(1, name))); 559 | }; 560 | 561 | tests.SUBSCRIBE = function () { 562 | var client1 = client, msg_count = 0, name = "SUBSCRIBE"; 563 | 564 | client1.on("subscribe", function (channel, count) { 565 | if (channel === "chan1") { 566 | // Callback assertions removed. 567 | // For some reason subscriber count is inconsistent here? 568 | // carlos8f: timeout for subscription to sync with master 569 | setTimeout(function () { 570 | client2.publish("chan1", "message 1"); 571 | client2.publish("chan2", "message 2"); 572 | client2.publish("chan1", "message 3"); 573 | }, 1000); 574 | } 575 | }); 576 | 577 | client1.on("unsubscribe", function (channel, count) { 578 | if (count === 0) { 579 | // make sure this connection can go into and out of pub/sub mode 580 | client1.incr("did a thing", last(name, require_number(2, name))); 581 | } 582 | }); 583 | 584 | client1.on("message", function (channel, message) { 585 | msg_count += 1; 586 | assert.strictEqual("message " + msg_count, message.toString()); 587 | if (msg_count === 3) { 588 | client1.unsubscribe("chan1", "chan2"); 589 | } 590 | }); 591 | 592 | client1.set("did a thing", 1, require_string("OK", name)); 593 | client1.subscribe("chan1", "chan2", function (err, results) { 594 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 595 | assert.strictEqual("chan1", results.toString(), name); 596 | }); 597 | }; 598 | 599 | tests.SUBSCRIBE_QUIT = function () { 600 | var name = "SUBSCRIBE_QUIT"; 601 | client3.on("end", function () { 602 | next(name); 603 | }); 604 | client3.on("subscribe", function (channel, count) { 605 | client3.quit(); 606 | }); 607 | client3.subscribe("chan3"); 608 | }; 609 | 610 | tests.EXISTS = function () { 611 | var name = "EXISTS"; 612 | client.del("foo", "foo2", require_number_any(name)); 613 | client.set("foo", "bar", require_string("OK", name)); 614 | client.EXISTS("foo", require_number(1, name)); 615 | client.EXISTS("foo2", last(name, require_number(0, name))); 616 | }; 617 | 618 | tests.DEL = function () { 619 | var name = "DEL"; 620 | client.DEL("delkey", require_number_any(name)); 621 | client.set("delkey", "delvalue", require_string("OK", name)); 622 | client.DEL("delkey", require_number(1, name)); 623 | client.exists("delkey", require_number(0, name)); 624 | client.DEL("delkey", require_number(0, name)); 625 | client.mset("delkey", "delvalue", "delkey2", "delvalue2", require_string("OK", name)); 626 | client.DEL("delkey", "delkey2", last(name, require_number(2, name))); 627 | }; 628 | 629 | tests.TYPE = function () { 630 | var name = "TYPE"; 631 | client.set(["string key", "should be a string"], require_string("OK", name)); 632 | client.rpush(["list key", "should be a list"], require_number_pos(name)); 633 | client.sadd(["set key", "should be a set"], require_number_any(name)); 634 | client.zadd(["zset key", "10.0", "should be a zset"], require_number_any(name)); 635 | client.hset(["hash key", "hashtest", "should be a hash"], require_number_any(0, name)); 636 | 637 | client.TYPE(["string key"], require_string("string", name)); 638 | client.TYPE(["list key"], require_string("list", name)); 639 | client.TYPE(["set key"], require_string("set", name)); 640 | client.TYPE(["zset key"], require_string("zset", name)); 641 | client.TYPE("not here yet", require_string("none", name)); 642 | client.TYPE(["hash key"], last(name, require_string("hash", name))); 643 | }; 644 | 645 | tests.KEYS = function () { 646 | var name = "KEYS"; 647 | client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], require_string("OK", name)); 648 | client.KEYS(["test keys*"], function (err, results) { 649 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 650 | assert.strictEqual(2, results.length, name); 651 | results.sort(); 652 | assert.strictEqual("test keys 1", results[0].toString(), name); 653 | assert.strictEqual("test keys 2", results[1].toString(), name); 654 | next(name); 655 | }); 656 | }; 657 | 658 | tests.MULTIBULK_ZERO_LENGTH = function () { 659 | var name = "MULTIBULK_ZERO_LENGTH"; 660 | client.KEYS(['users:*'], function (err, results) { 661 | assert.strictEqual(null, err, 'error on empty multibulk reply'); 662 | assert.strictEqual(true, is_empty_array(results), "not an empty array"); 663 | next(name); 664 | }); 665 | }; 666 | 667 | tests.RANDOMKEY = function () { 668 | var name = "RANDOMKEY"; 669 | client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], require_string("OK", name)); 670 | client.RANDOMKEY([], function (err, results) { 671 | assert.strictEqual(null, err, name + " result sent back unexpected error: " + err); 672 | assert.strictEqual(true, /\w+/.test(results), name); 673 | next(name); 674 | }); 675 | }; 676 | 677 | tests.RENAME = function () { 678 | var name = "RENAME"; 679 | client.set(['foo', 'bar'], require_string("OK", name)); 680 | client.RENAME(["foo", "new foo"], require_string("OK", name)); 681 | client.exists(["foo"], require_number(0, name)); 682 | client.exists(["new foo"], last(name, require_number(1, name))); 683 | }; 684 | 685 | tests.RENAMENX = function () { 686 | var name = "RENAMENX"; 687 | client.set(['foo', 'bar'], require_string("OK", name)); 688 | client.set(['foo2', 'bar2'], require_string("OK", name)); 689 | client.RENAMENX(["foo", "foo2"], require_number(0, name)); 690 | client.exists(["foo"], require_number(1, name)); 691 | client.exists(["foo2"], require_number(1, name)); 692 | client.del(["foo2"], require_number(1, name)); 693 | client.RENAMENX(["foo", "foo2"], require_number(1, name)); 694 | client.exists(["foo"], require_number(0, name)); 695 | client.exists(["foo2"], last(name, require_number(1, name))); 696 | }; 697 | 698 | tests.DBSIZE = function () { 699 | var name = "DBSIZE"; 700 | client.set(['foo', 'bar'], require_string("OK", name)); 701 | client.DBSIZE([], last(name, require_number_pos("DBSIZE"))); 702 | }; 703 | 704 | tests.GET = function () { 705 | var name = "GET"; 706 | client.set(["get key", "get val"], require_string("OK", name)); 707 | client.GET(["get key"], last(name, require_string("get val", name))); 708 | }; 709 | 710 | tests.SET = function () { 711 | var name = "SET"; 712 | client.SET(["set key", "set val"], require_string("OK", name)); 713 | client.get(["set key"], last(name, require_string("set val", name))); 714 | }; 715 | 716 | tests.GETSET = function () { 717 | var name = "GETSET"; 718 | client.set(["getset key", "getset val"], require_string("OK", name)); 719 | client.GETSET(["getset key", "new getset val"], require_string("getset val", name)); 720 | client.get(["getset key"], last(name, require_string("new getset val", name))); 721 | }; 722 | 723 | tests.MGET = function () { 724 | var name = "MGET"; 725 | client.mset(["mget keys 1", "mget val 1", "mget keys 2", "mget val 2", "mget keys 3", "mget val 3"], require_string("OK", name)); 726 | client.MGET("mget keys 1", "mget keys 2", "mget keys 3", function (err, results) { 727 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 728 | assert.strictEqual(3, results.length, name); 729 | assert.strictEqual("mget val 1", results[0].toString(), name); 730 | assert.strictEqual("mget val 2", results[1].toString(), name); 731 | assert.strictEqual("mget val 3", results[2].toString(), name); 732 | }); 733 | client.MGET(["mget keys 1", "mget keys 2", "mget keys 3"], function (err, results) { 734 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 735 | assert.strictEqual(3, results.length, name); 736 | assert.strictEqual("mget val 1", results[0].toString(), name); 737 | assert.strictEqual("mget val 2", results[1].toString(), name); 738 | assert.strictEqual("mget val 3", results[2].toString(), name); 739 | }); 740 | client.MGET(["mget keys 1", "some random shit", "mget keys 2", "mget keys 3"], function (err, results) { 741 | assert.strictEqual(null, err, "result sent back unexpected error: " + err); 742 | assert.strictEqual(4, results.length, name); 743 | assert.strictEqual("mget val 1", results[0].toString(), name); 744 | assert.strictEqual(null, results[1], name); 745 | assert.strictEqual("mget val 2", results[2].toString(), name); 746 | assert.strictEqual("mget val 3", results[3].toString(), name); 747 | next(name); 748 | }); 749 | }; 750 | 751 | tests.SETNX = function () { 752 | var name = "SETNX"; 753 | client.set(["setnx key", "setnx value"], require_string("OK", name)); 754 | client.SETNX(["setnx key", "new setnx value"], require_number(0, name)); 755 | client.del(["setnx key"], require_number(1, name)); 756 | client.exists(["setnx key"], require_number(0, name)); 757 | client.SETNX(["setnx key", "new setnx value"], require_number(1, name)); 758 | client.exists(["setnx key"], last(name, require_number(1, name))); 759 | }; 760 | 761 | tests.SETEX = function () { 762 | var name = "SETEX"; 763 | client.SETEX(["setex key", "100", "setex val"], require_string("OK", name)); 764 | client.exists(["setex key"], require_number(1, name)); 765 | client.ttl(["setex key"], last(name, require_number_pos(name))); 766 | }; 767 | 768 | tests.MSETNX = function () { 769 | var name = "MSETNX"; 770 | client.mset(["mset1", "val1", "mset2", "val2", "mset3", "val3"], require_string("OK", name)); 771 | client.MSETNX(["mset3", "val3", "mset4", "val4"], require_number(0, name)); 772 | client.del(["mset3"], require_number(1, name)); 773 | client.MSETNX(["mset3", "val3", "mset4", "val4"], require_number(1, name)); 774 | client.exists(["mset3"], require_number(1, name)); 775 | client.exists(["mset4"], last(name, require_number(1, name))); 776 | }; 777 | 778 | tests.HGETALL = function () { 779 | var name = "HGETALL"; 780 | client.hmset(["hosts", "mjr", "1", "another", "23", "home", "1234"], require_string("OK", name)); 781 | client.HGETALL(["hosts"], function (err, obj) { 782 | assert.strictEqual(null, err, name + " result sent back unexpected error: " + err); 783 | assert.strictEqual(3, Object.keys(obj).length, name); 784 | assert.strictEqual("1", obj.mjr.toString(), name); 785 | assert.strictEqual("23", obj.another.toString(), name); 786 | assert.strictEqual("1234", obj.home.toString(), name); 787 | next(name); 788 | }); 789 | }; 790 | 791 | tests.HGETALL_NULL = function () { 792 | var name = "HGETALL_NULL"; 793 | 794 | client.hgetall("missing", function (err, obj) { 795 | assert.strictEqual(null, err); 796 | assert.strictEqual(null, obj); 797 | next(name); 798 | }); 799 | }; 800 | 801 | tests.UTF8 = function () { 802 | var name = "UTF8", 803 | utf8_sample = "ಠ_ಠ"; 804 | 805 | client.set(["utf8test", utf8_sample], require_string("OK", name)); 806 | client.get(["utf8test"], function (err, obj) { 807 | assert.strictEqual(null, err); 808 | assert.strictEqual(utf8_sample, obj); 809 | next(name); 810 | }); 811 | }; 812 | 813 | // Set tests were adapted from Brian Hammond's redis-node-client.js, which has a comprehensive test suite 814 | 815 | tests.SADD = function () { 816 | var name = "SADD"; 817 | 818 | client.del('set0'); 819 | client.sadd('set0', 'member0', require_number(1, name)); 820 | client.sadd('set0', 'member0', last(name, require_number(0, name))); 821 | }; 822 | 823 | tests.SADD2 = function () { 824 | var name = "SADD2"; 825 | 826 | client.del("set0"); 827 | client.sadd("set0", ["member0", "member1", "member2"], require_number(3, name)); 828 | client.smembers("set0", function (err, res) { 829 | assert.strictEqual(res.length, 3); 830 | res.sort(); 831 | assert.strictEqual(res[0], "member0"); 832 | assert.strictEqual(res[1], "member1"); 833 | assert.strictEqual(res[2], "member2"); 834 | next(name); 835 | }); 836 | }; 837 | 838 | tests.SISMEMBER = function () { 839 | var name = "SISMEMBER"; 840 | 841 | client.del('set0'); 842 | client.sadd('set0', 'member0', require_number(1, name)); 843 | client.sismember('set0', 'member0', require_number(1, name)); 844 | client.sismember('set0', 'member1', last(name, require_number(0, name))); 845 | }; 846 | 847 | tests.SCARD = function () { 848 | var name = "SCARD"; 849 | 850 | client.del('set0'); 851 | client.sadd('set0', 'member0', require_number(1, name)); 852 | client.scard('set0', require_number(1, name)); 853 | client.sadd('set0', 'member1', require_number(1, name)); 854 | client.scard('set0', last(name, require_number(2, name))); 855 | }; 856 | 857 | tests.SREM = function () { 858 | var name = "SREM"; 859 | 860 | client.del('set0'); 861 | client.sadd('set0', 'member0', require_number(1, name)); 862 | client.srem('set0', 'foobar', require_number(0, name)); 863 | client.srem('set0', 'member0', require_number(1, name)); 864 | client.scard('set0', last(name, require_number(0, name))); 865 | }; 866 | 867 | tests.SPOP = function () { 868 | var name = "SPOP"; 869 | 870 | client.del('zzz'); 871 | client.sadd('zzz', 'member0', require_number(1, name)); 872 | client.scard('zzz', require_number(1, name)); 873 | 874 | client.spop('zzz', function (err, value) { 875 | if (err) { 876 | assert.fail(err); 877 | } 878 | assert.equal(value, 'member0', name); 879 | }); 880 | 881 | client.scard('zzz', last(name, require_number(0, name))); 882 | }; 883 | 884 | tests.SDIFF = function () { 885 | var name = "SDIFF"; 886 | 887 | client.del('foo'); 888 | client.sadd('foo', 'x', require_number(1, name)); 889 | client.sadd('foo', 'a', require_number(1, name)); 890 | client.sadd('foo', 'b', require_number(1, name)); 891 | client.sadd('foo', 'c', require_number(1, name)); 892 | 893 | client.sadd('bar', 'c', require_number(1, name)); 894 | 895 | client.sadd('baz', 'a', require_number(1, name)); 896 | client.sadd('baz', 'd', require_number(1, name)); 897 | 898 | client.sdiff('foo', 'bar', 'baz', function (err, values) { 899 | if (err) { 900 | assert.fail(err, name); 901 | } 902 | values.sort(); 903 | assert.equal(values.length, 2, name); 904 | assert.equal(values[0], 'b', name); 905 | assert.equal(values[1], 'x', name); 906 | next(name); 907 | }); 908 | }; 909 | 910 | tests.SDIFFSTORE = function () { 911 | var name = "SDIFFSTORE"; 912 | 913 | client.del('foo'); 914 | client.del('bar'); 915 | client.del('baz'); 916 | client.del('quux'); 917 | 918 | client.sadd('foo', 'x', require_number(1, name)); 919 | client.sadd('foo', 'a', require_number(1, name)); 920 | client.sadd('foo', 'b', require_number(1, name)); 921 | client.sadd('foo', 'c', require_number(1, name)); 922 | 923 | client.sadd('bar', 'c', require_number(1, name)); 924 | 925 | client.sadd('baz', 'a', require_number(1, name)); 926 | client.sadd('baz', 'd', require_number(1, name)); 927 | 928 | // NB: SDIFFSTORE returns the number of elements in the dstkey 929 | 930 | client.sdiffstore('quux', 'foo', 'bar', 'baz', require_number(2, name)); 931 | 932 | client.smembers('quux', function (err, values) { 933 | if (err) { 934 | assert.fail(err, name); 935 | } 936 | var members = buffers_to_strings(values).sort(); 937 | 938 | assert.deepEqual(members, [ 'b', 'x' ], name); 939 | next(name); 940 | }); 941 | }; 942 | 943 | tests.SMEMBERS = function () { 944 | var name = "SMEMBERS"; 945 | 946 | client.del('foo'); 947 | client.sadd('foo', 'x', require_number(1, name)); 948 | 949 | client.smembers('foo', function (err, members) { 950 | if (err) { 951 | assert.fail(err, name); 952 | } 953 | assert.deepEqual(buffers_to_strings(members), [ 'x' ], name); 954 | }); 955 | 956 | client.sadd('foo', 'y', require_number(1, name)); 957 | 958 | client.smembers('foo', function (err, values) { 959 | if (err) { 960 | assert.fail(err, name); 961 | } 962 | assert.equal(values.length, 2, name); 963 | var members = buffers_to_strings(values).sort(); 964 | 965 | assert.deepEqual(members, [ 'x', 'y' ], name); 966 | next(name); 967 | }); 968 | }; 969 | 970 | tests.SMOVE = function () { 971 | var name = "SMOVE"; 972 | 973 | client.del('foo'); 974 | client.del('bar'); 975 | 976 | client.sadd('foo', 'x', require_number(1, name)); 977 | client.smove('foo', 'bar', 'x', require_number(1, name)); 978 | client.sismember('foo', 'x', require_number(0, name)); 979 | client.sismember('bar', 'x', require_number(1, name)); 980 | client.smove('foo', 'bar', 'x', last(name, require_number(0, name))); 981 | }; 982 | 983 | tests.SINTER = function () { 984 | var name = "SINTER"; 985 | 986 | client.del('sa'); 987 | client.del('sb'); 988 | client.del('sc'); 989 | 990 | client.sadd('sa', 'a', require_number(1, name)); 991 | client.sadd('sa', 'b', require_number(1, name)); 992 | client.sadd('sa', 'c', require_number(1, name)); 993 | 994 | client.sadd('sb', 'b', require_number(1, name)); 995 | client.sadd('sb', 'c', require_number(1, name)); 996 | client.sadd('sb', 'd', require_number(1, name)); 997 | 998 | client.sadd('sc', 'c', require_number(1, name)); 999 | client.sadd('sc', 'd', require_number(1, name)); 1000 | client.sadd('sc', 'e', require_number(1, name)); 1001 | 1002 | client.sinter('sa', 'sb', function (err, intersection) { 1003 | if (err) { 1004 | assert.fail(err, name); 1005 | } 1006 | assert.equal(intersection.length, 2, name); 1007 | assert.deepEqual(buffers_to_strings(intersection).sort(), [ 'b', 'c' ], name); 1008 | }); 1009 | 1010 | client.sinter('sb', 'sc', function (err, intersection) { 1011 | if (err) { 1012 | assert.fail(err, name); 1013 | } 1014 | assert.equal(intersection.length, 2, name); 1015 | assert.deepEqual(buffers_to_strings(intersection).sort(), [ 'c', 'd' ], name); 1016 | }); 1017 | 1018 | client.sinter('sa', 'sc', function (err, intersection) { 1019 | if (err) { 1020 | assert.fail(err, name); 1021 | } 1022 | assert.equal(intersection.length, 1, name); 1023 | assert.equal(intersection[0], 'c', name); 1024 | }); 1025 | 1026 | // 3-way 1027 | 1028 | client.sinter('sa', 'sb', 'sc', function (err, intersection) { 1029 | if (err) { 1030 | assert.fail(err, name); 1031 | } 1032 | assert.equal(intersection.length, 1, name); 1033 | assert.equal(intersection[0], 'c', name); 1034 | next(name); 1035 | }); 1036 | }; 1037 | 1038 | tests.SINTERSTORE = function () { 1039 | var name = "SINTERSTORE"; 1040 | 1041 | client.del('sa'); 1042 | client.del('sb'); 1043 | client.del('sc'); 1044 | client.del('foo'); 1045 | 1046 | client.sadd('sa', 'a', require_number(1, name)); 1047 | client.sadd('sa', 'b', require_number(1, name)); 1048 | client.sadd('sa', 'c', require_number(1, name)); 1049 | 1050 | client.sadd('sb', 'b', require_number(1, name)); 1051 | client.sadd('sb', 'c', require_number(1, name)); 1052 | client.sadd('sb', 'd', require_number(1, name)); 1053 | 1054 | client.sadd('sc', 'c', require_number(1, name)); 1055 | client.sadd('sc', 'd', require_number(1, name)); 1056 | client.sadd('sc', 'e', require_number(1, name)); 1057 | 1058 | client.sinterstore('foo', 'sa', 'sb', 'sc', require_number(1, name)); 1059 | 1060 | client.smembers('foo', function (err, members) { 1061 | if (err) { 1062 | assert.fail(err, name); 1063 | } 1064 | assert.deepEqual(buffers_to_strings(members), [ 'c' ], name); 1065 | next(name); 1066 | }); 1067 | }; 1068 | 1069 | tests.SUNION = function () { 1070 | var name = "SUNION"; 1071 | 1072 | client.del('sa'); 1073 | client.del('sb'); 1074 | client.del('sc'); 1075 | 1076 | client.sadd('sa', 'a', require_number(1, name)); 1077 | client.sadd('sa', 'b', require_number(1, name)); 1078 | client.sadd('sa', 'c', require_number(1, name)); 1079 | 1080 | client.sadd('sb', 'b', require_number(1, name)); 1081 | client.sadd('sb', 'c', require_number(1, name)); 1082 | client.sadd('sb', 'd', require_number(1, name)); 1083 | 1084 | client.sadd('sc', 'c', require_number(1, name)); 1085 | client.sadd('sc', 'd', require_number(1, name)); 1086 | client.sadd('sc', 'e', require_number(1, name)); 1087 | 1088 | client.sunion('sa', 'sb', 'sc', function (err, union) { 1089 | if (err) { 1090 | assert.fail(err, name); 1091 | } 1092 | assert.deepEqual(buffers_to_strings(union).sort(), ['a', 'b', 'c', 'd', 'e'], name); 1093 | next(name); 1094 | }); 1095 | }; 1096 | 1097 | tests.SUNIONSTORE = function () { 1098 | var name = "SUNIONSTORE"; 1099 | 1100 | client.del('sa'); 1101 | client.del('sb'); 1102 | client.del('sc'); 1103 | client.del('foo'); 1104 | 1105 | client.sadd('sa', 'a', require_number(1, name)); 1106 | client.sadd('sa', 'b', require_number(1, name)); 1107 | client.sadd('sa', 'c', require_number(1, name)); 1108 | 1109 | client.sadd('sb', 'b', require_number(1, name)); 1110 | client.sadd('sb', 'c', require_number(1, name)); 1111 | client.sadd('sb', 'd', require_number(1, name)); 1112 | 1113 | client.sadd('sc', 'c', require_number(1, name)); 1114 | client.sadd('sc', 'd', require_number(1, name)); 1115 | client.sadd('sc', 'e', require_number(1, name)); 1116 | 1117 | client.sunionstore('foo', 'sa', 'sb', 'sc', function (err, cardinality) { 1118 | if (err) { 1119 | assert.fail(err, name); 1120 | } 1121 | assert.equal(cardinality, 5, name); 1122 | }); 1123 | 1124 | client.smembers('foo', function (err, members) { 1125 | if (err) { 1126 | assert.fail(err, name); 1127 | } 1128 | assert.equal(members.length, 5, name); 1129 | assert.deepEqual(buffers_to_strings(members).sort(), ['a', 'b', 'c', 'd', 'e'], name); 1130 | next(name); 1131 | }); 1132 | }; 1133 | 1134 | // SORT test adapted from Brian Hammond's redis-node-client.js, which has a comprehensive test suite 1135 | 1136 | tests.SORT = function () { 1137 | var name = "SORT"; 1138 | 1139 | client.del('y'); 1140 | client.del('x'); 1141 | 1142 | client.rpush('y', 'd', require_number(1, name)); 1143 | client.rpush('y', 'b', require_number(2, name)); 1144 | client.rpush('y', 'a', require_number(3, name)); 1145 | client.rpush('y', 'c', require_number(4, name)); 1146 | 1147 | client.rpush('x', '3', require_number(1, name)); 1148 | client.rpush('x', '9', require_number(2, name)); 1149 | client.rpush('x', '2', require_number(3, name)); 1150 | client.rpush('x', '4', require_number(4, name)); 1151 | 1152 | client.set('w3', '4', require_string("OK", name)); 1153 | client.set('w9', '5', require_string("OK", name)); 1154 | client.set('w2', '12', require_string("OK", name)); 1155 | client.set('w4', '6', require_string("OK", name)); 1156 | 1157 | client.set('o2', 'buz', require_string("OK", name)); 1158 | client.set('o3', 'foo', require_string("OK", name)); 1159 | client.set('o4', 'baz', require_string("OK", name)); 1160 | client.set('o9', 'bar', require_string("OK", name)); 1161 | 1162 | client.set('p2', 'qux', require_string("OK", name)); 1163 | client.set('p3', 'bux', require_string("OK", name)); 1164 | client.set('p4', 'lux', require_string("OK", name)); 1165 | client.set('p9', 'tux', require_string("OK", name)); 1166 | 1167 | // Now the data has been setup, we can test. 1168 | 1169 | // But first, test basic sorting. 1170 | 1171 | // y = [ d b a c ] 1172 | // sort y ascending = [ a b c d ] 1173 | // sort y descending = [ d c b a ] 1174 | 1175 | client.sort('y', 'asc', 'alpha', function (err, sorted) { 1176 | if (err) { 1177 | assert.fail(err, name); 1178 | } 1179 | assert.deepEqual(buffers_to_strings(sorted), ['a', 'b', 'c', 'd'], name); 1180 | }); 1181 | 1182 | client.sort('y', 'desc', 'alpha', function (err, sorted) { 1183 | if (err) { 1184 | assert.fail(err, name); 1185 | } 1186 | assert.deepEqual(buffers_to_strings(sorted), ['d', 'c', 'b', 'a'], name); 1187 | }); 1188 | 1189 | // Now try sorting numbers in a list. 1190 | // x = [ 3, 9, 2, 4 ] 1191 | 1192 | client.sort('x', 'asc', function (err, sorted) { 1193 | if (err) { 1194 | assert.fail(err, name); 1195 | } 1196 | assert.deepEqual(buffers_to_strings(sorted), [2, 3, 4, 9], name); 1197 | }); 1198 | 1199 | client.sort('x', 'desc', function (err, sorted) { 1200 | if (err) { 1201 | assert.fail(err, name); 1202 | } 1203 | assert.deepEqual(buffers_to_strings(sorted), [9, 4, 3, 2], name); 1204 | }); 1205 | 1206 | // Try sorting with a 'by' pattern. 1207 | 1208 | client.sort('x', 'by', 'w*', 'asc', function (err, sorted) { 1209 | if (err) { 1210 | assert.fail(err, name); 1211 | } 1212 | assert.deepEqual(buffers_to_strings(sorted), [3, 9, 4, 2], name); 1213 | }); 1214 | 1215 | // Try sorting with a 'by' pattern and 1 'get' pattern. 1216 | 1217 | client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', function (err, sorted) { 1218 | if (err) { 1219 | assert.fail(err, name); 1220 | } 1221 | assert.deepEqual(buffers_to_strings(sorted), ['foo', 'bar', 'baz', 'buz'], name); 1222 | }); 1223 | 1224 | // Try sorting with a 'by' pattern and 2 'get' patterns. 1225 | 1226 | client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', function (err, sorted) { 1227 | if (err) { 1228 | assert.fail(err, name); 1229 | } 1230 | assert.deepEqual(buffers_to_strings(sorted), ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux'], name); 1231 | }); 1232 | 1233 | // Try sorting with a 'by' pattern and 2 'get' patterns. 1234 | // Instead of getting back the sorted set/list, store the values to a list. 1235 | // Then check that the values are there in the expected order. 1236 | 1237 | client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', 'store', 'bacon', function (err) { 1238 | if (err) { 1239 | assert.fail(err, name); 1240 | } 1241 | }); 1242 | 1243 | client.lrange('bacon', 0, -1, function (err, values) { 1244 | if (err) { 1245 | assert.fail(err, name); 1246 | } 1247 | assert.deepEqual(buffers_to_strings(values), ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux'], name); 1248 | next(name); 1249 | }); 1250 | 1251 | // TODO - sort by hash value 1252 | }; 1253 | 1254 | tests.MONITOR = function () { 1255 | var name = "MONITOR", responses = [], monitor_client; 1256 | 1257 | if (client.server_info.versions[0] == 2 && client.server_info.versions[1] <= 4) { 1258 | monitor_client = redis.createClient(nodes); 1259 | monitor_client.monitor(function (err, res) { 1260 | client.mget("some", "keys", "foo", "bar"); 1261 | client.set("json", JSON.stringify({ 1262 | foo: "123", 1263 | bar: "sdflkdfsjk", 1264 | another: false 1265 | })); 1266 | }); 1267 | monitor_client.on("monitor", function (time, args) { 1268 | responses.push(args); 1269 | if (responses.length === 3) { 1270 | assert.strictEqual(1, responses[0].length); 1271 | assert.strictEqual("monitor", responses[0][0]); 1272 | assert.strictEqual(5, responses[1].length); 1273 | assert.strictEqual("mget", responses[1][0]); 1274 | assert.strictEqual("some", responses[1][1]); 1275 | assert.strictEqual("keys", responses[1][2]); 1276 | assert.strictEqual("foo", responses[1][3]); 1277 | assert.strictEqual("bar", responses[1][4]); 1278 | assert.strictEqual(3, responses[2].length); 1279 | assert.strictEqual("set", responses[2][0]); 1280 | assert.strictEqual("json", responses[2][1]); 1281 | assert.strictEqual('{"foo":"123","bar":"sdflkdfsjk","another":false}', responses[2][2]); 1282 | monitor_client.quit(function (err, res) { 1283 | next(name); 1284 | }); 1285 | } 1286 | }); 1287 | } 1288 | else { 1289 | console.log("Skipping " + name + " because server version seems to choke on it."); 1290 | next(name); 1291 | } 1292 | }; 1293 | 1294 | tests.BLPOP = function () { 1295 | var name = "BLPOP"; 1296 | 1297 | client.rpush("blocking list", "initial value", function (err, res) { 1298 | client2.BLPOP("blocking list", 0, function (err, res) { 1299 | assert.strictEqual("blocking list", res[0].toString()); 1300 | assert.strictEqual("initial value", res[1].toString()); 1301 | 1302 | client.rpush("blocking list", "wait for this value"); 1303 | }); 1304 | client2.BLPOP("blocking list", 0, function (err, res) { 1305 | assert.strictEqual("blocking list", res[0].toString()); 1306 | assert.strictEqual("wait for this value", res[1].toString()); 1307 | next(name); 1308 | }); 1309 | }); 1310 | }; 1311 | 1312 | tests.BLPOP_TIMEOUT = function () { 1313 | var name = "BLPOP_TIMEOUT"; 1314 | 1315 | // try to BLPOP the list again, which should be empty. This should timeout and return null. 1316 | client2.BLPOP("blocking list", 1, function (err, res) { 1317 | if (err) { 1318 | throw err; 1319 | } 1320 | 1321 | assert.strictEqual(res, null); 1322 | next(name); 1323 | }); 1324 | }; 1325 | 1326 | tests.EXPIRE = function () { 1327 | var name = "EXPIRE"; 1328 | client.set(['expiry key', 'bar'], require_string("OK", name)); 1329 | client.EXPIRE(["expiry key", "1"], require_number_pos(name)); 1330 | setTimeout(function () { 1331 | client.exists(["expiry key"], last(name, require_number(0, name))); 1332 | }, 2000); 1333 | }; 1334 | 1335 | tests.TTL = function () { 1336 | var name = "TTL"; 1337 | client.set(["ttl key", "ttl val"], require_string("OK", name)); 1338 | client.expire(["ttl key", "100"], require_number_pos(name)); 1339 | setTimeout(function () { 1340 | client.TTL(["ttl key"], last(name, require_number_pos(0, name))); 1341 | }, 500); 1342 | }; 1343 | 1344 | tests.OPTIONAL_CALLBACK = function () { 1345 | var name = "OPTIONAL_CALLBACK"; 1346 | client.del("op_cb1"); 1347 | client.set("op_cb1", "x"); 1348 | client.get("op_cb1", last(name, require_string("x", name))); 1349 | }; 1350 | 1351 | tests.OPTIONAL_CALLBACK_UNDEFINED = function () { 1352 | var name = "OPTIONAL_CALLBACK_UNDEFINED"; 1353 | client.del("op_cb2"); 1354 | client.set("op_cb2", "y", undefined); 1355 | client.get("op_cb2", last(name, require_string("y", name))); 1356 | }; 1357 | 1358 | // TODO - need a better way to test auth, maybe auto-config a local Redis server or something. 1359 | // Yes, this is the real password. Please be nice, thanks. 1360 | /* disabled because this is not runnable on travis - carlos8f 1361 | tests.auth = function () { 1362 | var name = "AUTH", client4, ready_count = 0; 1363 | 1364 | client4 = redis.createClient(9006, "filefish.redistogo.com"); 1365 | client4.auth("664b1b6aaf134e1ec281945a8de702a9", function (err, res) { 1366 | assert.strictEqual(null, err, name); 1367 | assert.strictEqual("OK", res.toString(), name); 1368 | }); 1369 | 1370 | // test auth, then kill the connection so it'll auto-reconnect and auto-re-auth 1371 | client4.on("ready", function () { 1372 | ready_count++; 1373 | if (ready_count === 1) { 1374 | client4.stream.destroy(); 1375 | } else { 1376 | client4.quit(function (err, res) { 1377 | next(name); 1378 | }); 1379 | } 1380 | }); 1381 | }; 1382 | */ 1383 | 1384 | all_tests = Object.keys(tests); 1385 | all_start = new Date(); 1386 | test_count = 0; 1387 | 1388 | run_next_test = function run_next_test() { 1389 | var test_name = all_tests.shift(); 1390 | if (typeof tests[test_name] === "function") { 1391 | util.print('- \x1b[1m' + test_name.toLowerCase() + '\x1b[0m:'); 1392 | cur_start = new Date(); 1393 | test_count += 1; 1394 | tests[test_name](); 1395 | } else { 1396 | console.log('\n completed \x1b[32m%d\x1b[0m tests in \x1b[33m%d\x1b[0m ms\n', test_count, new Date() - all_start); 1397 | client.quit(); 1398 | client2.quit(); 1399 | } 1400 | }; 1401 | 1402 | client.once("ready", function start_tests() { 1403 | console.log("Connected to " + client.host + ":" + client.port + ", Redis server version " + client.server_info.redis_version + "\n"); 1404 | console.log("Using reply parser " + client.reply_parser.name); 1405 | 1406 | run_next_test(); 1407 | 1408 | connected = true; 1409 | }); 1410 | 1411 | client.on('end', function () { 1412 | ended = true; 1413 | }); 1414 | 1415 | // Exit immediately on connection failure, which triggers "exit", below, which fails the test 1416 | client.on("error", function (err) { 1417 | console.error("client: " + err.stack); 1418 | process.exit(); 1419 | }); 1420 | client2.on("error", function (err) { 1421 | console.error("client2: " + err.stack); 1422 | process.exit(); 1423 | }); 1424 | client3.on("error", function (err) { 1425 | console.error("client3: " + err.stack); 1426 | process.exit(); 1427 | }); 1428 | client.on("reconnecting", function (params) { 1429 | console.log("reconnecting: " + util.inspect(params)); 1430 | }); 1431 | 1432 | process.on('uncaughtException', function (err) { 1433 | console.error("Uncaught exception: " + err.stack); 1434 | process.exit(1); 1435 | }); 1436 | 1437 | process.on('exit', function (code) { 1438 | assert.equal(true, connected); 1439 | assert.equal(true, ended); 1440 | }); 1441 | -------------------------------------------------------------------------------- /test/_common.js: -------------------------------------------------------------------------------- 1 | assert = require('assert'); 2 | haredis = require('../'); 3 | async = require('async'); 4 | tmp = require('haredis-tmp'); 5 | path = require('path'); 6 | spawn = require('child_process').spawn; 7 | 8 | servers = {}; 9 | ports = ['127.0.0.1:6380', 'localhost:6381', 6382]; 10 | var shutdown; 11 | 12 | shutdownServers = function (done) { 13 | shutdown(function (err) { 14 | assert.ifError(err); 15 | delete shutdown; 16 | servers = {}; 17 | done(); 18 | }); 19 | }; 20 | 21 | makeServers = function (done) { 22 | var _ports = ports.map(function (port) { 23 | return typeof port === 'string' ? Number(port.split(':')[1]) : port; 24 | }); 25 | tmp(_ports, function (err, p, sd, s) { 26 | assert.ifError(err); 27 | shutdown = sd; 28 | servers = s; 29 | done(); 30 | }); 31 | }; 32 | 33 | createClient = function () { 34 | var client = haredis.createClient(ports); 35 | // redirect logging 36 | client.warn = client.error = client.log = log; 37 | 38 | client.tail = ''; 39 | 40 | function log () { 41 | var args = [].slice.call(arguments); 42 | var line = args.join(' ') + '\n'; 43 | client.tail += line; 44 | //process.stdout.write(line); 45 | }; 46 | 47 | client.waitFor = function (pattern, cb) { 48 | if (Array.isArray(pattern)) { 49 | var list = pattern.slice(); 50 | var latch = list.length; 51 | (function doNext () { 52 | var pat = list.shift(); 53 | client.waitFor(pat, function () { 54 | if (!--latch) cb(); 55 | else doNext(); 56 | }); 57 | })(); 58 | return; 59 | } 60 | var timeout, found = false; 61 | (function search () { 62 | if (found) return; 63 | client.tail.split('\n').forEach(function (line, idx) { 64 | if (found) return; 65 | if (line.match(pattern)) { 66 | found = true; 67 | client.tail = client.tail.split('\n').slice(idx).join('\n'); 68 | cb(); 69 | } 70 | }); 71 | if (!found) timeout = setTimeout(search, 500); 72 | })(); 73 | setTimeout(function () { 74 | clearTimeout(timeout); 75 | if (!found) throw new Error('timed out waiting for `' + pattern + '`'); 76 | }, 30000); 77 | }; 78 | 79 | return client; 80 | }; 81 | -------------------------------------------------------------------------------- /test/failover.js: -------------------------------------------------------------------------------- 1 | describe('failover', function () { 2 | before(makeServers); 3 | after(shutdownServers); 4 | 5 | var client, masterPort, newMasterPort; 6 | it('create client', function (done) { 7 | client = createClient(); 8 | 9 | var latch = 2; 10 | client.once('connect', function () { 11 | masterPort = client.master.port; 12 | if (!--latch) done(); 13 | }); 14 | 15 | client.waitFor([ 16 | 'invalid master count: 3', 17 | 'elected .* as master!', 18 | 'making .* into a slave...', 19 | 'making .* into a slave...' 20 | ], function () { 21 | if (!--latch) done(); 22 | }); 23 | }); 24 | 25 | it('increment counter', function (done) { 26 | client.INCR('test-counter', done); 27 | }); 28 | 29 | it('wait a bit for replication', function (done) { 30 | setTimeout(done, 1500); 31 | }); 32 | 33 | it('check counter on nodes', function (done) { 34 | var latch = 3; 35 | assert.equal(client.up.length, latch); 36 | client.up.forEach(function (node) { 37 | node.client.GET('test-counter', function (err, counter) { 38 | assert.ifError(err); 39 | assert.equal(counter, 1); 40 | if (!--latch) done(); 41 | }); 42 | }); 43 | }); 44 | 45 | it('kill master', function (done) { 46 | client.waitFor([ 47 | 'MASTER connection dropped, reconnecting...', 48 | 'Error: .* connection dropped and reconnection failed!', 49 | 'MASTER is down!', 50 | 'reorientating', 51 | 'invalid master count: 0', 52 | 'orientate complete, using .* as master' 53 | ], done); 54 | servers[masterPort].kill(); 55 | delete servers[masterPort]; 56 | }); 57 | 58 | it('check counter', function (done) { 59 | var latch = 2; 60 | assert.equal(client.up.length, latch); 61 | client.up.forEach(function (node) { 62 | node.client.GET('test-counter', function (err, counter) { 63 | assert.ifError(err); 64 | assert.equal(counter, 1); 65 | if (!--latch) done(); 66 | }); 67 | }); 68 | }); 69 | 70 | it('client had failed over', function () { 71 | newMasterPort = client.master.port; 72 | assert(newMasterPort != masterPort); 73 | assert(client.ready); 74 | assert(client.connected); 75 | }); 76 | 77 | it('increment counter again', function (done) { 78 | client.INCR('test-counter', done); 79 | }); 80 | 81 | it('restart old master', function (done) { 82 | tmp([masterPort], function (err, p, sd, s) { 83 | assert.ifError(err); 84 | servers[masterPort] = s[masterPort]; 85 | done(); 86 | }); 87 | }); 88 | 89 | it('old master is made into slave', function (done) { 90 | // @todo: waitFor message that masterPort made into slave 91 | client.waitFor([ 92 | 'invalid master count: 2', 93 | 'attempting failover!', 94 | 'elected .* as master!', 95 | 'orientate complete' 96 | ], done); 97 | }); 98 | 99 | it('cluster in reasonable state', function () { 100 | assert.equal(client.nodes.length, 3); 101 | assert.equal(client.up.length, 3); 102 | var masterCount = 0; 103 | client.nodes.forEach(function (node) { 104 | if (node.port === masterPort) assert.equal(node.role, 'slave'); 105 | if (node.role === 'master') masterCount++; 106 | }); 107 | assert.equal(masterCount, 1); 108 | }); 109 | 110 | it('wait a bit for replication', function (done) { 111 | setTimeout(done, 1500); 112 | }); 113 | 114 | it('get counter', function (done) { 115 | var latch = 3; 116 | assert.equal(client.up.length, latch); 117 | assert(client.connected); 118 | client.up.forEach(function (node) { 119 | node.client.GET('test-counter', function (err, counter) { 120 | assert.ifError(err); 121 | if (counter != 2) console.log('bad counter!', node + ''); 122 | assert.equal(counter, 2); 123 | if (!--latch) done(); 124 | }); 125 | }); 126 | }); 127 | 128 | it('quit', function (done) { 129 | client.quit(done); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/node_redis.js: -------------------------------------------------------------------------------- 1 | describe('node_redis tests', function () { 2 | before(makeServers); 3 | after(shutdownServers); 4 | 5 | it('original node_redis test', function (done) { 6 | var child = spawn('node', [path.join(__dirname, '..', 'test-singlemode.js')]); 7 | child.stderr.pipe(process.stderr); 8 | child.once('exit', function (code) { 9 | assert.equal(code, 0); 10 | done(); 11 | }); 12 | }); 13 | 14 | it('node_redis test in cluster mode', function (done) { 15 | var child = spawn('node', [path.join(__dirname, '..', 'test.js')]); 16 | child.stderr.pipe(process.stderr); 17 | child.once('exit', function (code) { 18 | assert.equal(code, 0); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/quit.js: -------------------------------------------------------------------------------- 1 | describe('quit', function () { 2 | before(makeServers); 3 | after(shutdownServers); 4 | 5 | describe('clustered', function () { 6 | var client, ended = false; 7 | it('create client', function (done) { 8 | client = createClient(); 9 | client.once('connect', done); 10 | }); 11 | it('kill master', function (done) { 12 | servers[client.master.port].kill(); 13 | delete servers[client.master.port]; 14 | setTimeout(done, 2000); 15 | }); 16 | it('quit', function (done) { 17 | client.once('end', function () { 18 | ended = true; 19 | }); 20 | client.quit(done); 21 | }); 22 | it('ended', function (done) { 23 | setTimeout(function () { 24 | assert(ended); 25 | done(); 26 | }, 1000); 27 | }); 28 | }); 29 | 30 | describe('single', function () { 31 | var client, ended = false; 32 | it('create client', function (done) { 33 | client = haredis.createClient(); 34 | client.once('connect', done); 35 | }); 36 | it('quit', function (done) { 37 | client.once('end', function () { 38 | ended = true; 39 | }); 40 | client.quit(done); 41 | }); 42 | it('ended', function (done) { 43 | setTimeout(function () { 44 | assert(ended); 45 | done(); 46 | }, 1000); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('end', function () { 52 | before(makeServers); 53 | after(shutdownServers); 54 | 55 | describe('clustered', function () { 56 | var client; 57 | it('create client', function (done) { 58 | client = createClient(); 59 | client.once('connect', done); 60 | }); 61 | it('kill master', function (done) { 62 | servers[client.master.port].kill(); 63 | delete servers[client.master.port]; 64 | setTimeout(done, 2000); 65 | }); 66 | it('end', function (done) { 67 | client.once('end', done); 68 | client.end(); 69 | }); 70 | }); 71 | 72 | describe('single', function () { 73 | var client; 74 | it('create client', function (done) { 75 | client = haredis.createClient(); 76 | client.once('connect', done); 77 | }); 78 | it('quit', function (done) { 79 | client.once('end', done); 80 | client.end(); 81 | }); 82 | }); 83 | }); --------------------------------------------------------------------------------