redis 0.6.7 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.npmignore +1 -0
  2. package/README.md +172 -48
  3. package/{tests → benches}/buffer_bench.js +0 -0
  4. package/benches/hiredis_parser.js +38 -0
  5. package/benches/re_sub_test.js +14 -0
  6. package/{tests → benches}/reconnect_test.js +4 -2
  7. package/{tests → benches}/stress/codec.js +0 -0
  8. package/{tests → benches}/stress/pubsub/pub.js +0 -0
  9. package/{tests → benches}/stress/pubsub/run +0 -0
  10. package/{tests → benches}/stress/pubsub/server.js +0 -0
  11. package/{tests → benches}/stress/rpushblpop/pub.js +0 -0
  12. package/{tests → benches}/stress/rpushblpop/run +0 -0
  13. package/{tests → benches}/stress/rpushblpop/server.js +0 -0
  14. package/{tests → benches}/stress/speed/00 +0 -0
  15. package/{tests → benches}/stress/speed/plot +0 -0
  16. package/{tests → benches}/stress/speed/size-rate.png +0 -0
  17. package/{tests → benches}/stress/speed/speed.js +0 -0
  18. package/{tests → benches}/sub_quit_test.js +0 -0
  19. package/changelog.md +35 -0
  20. package/diff_multi_bench_output.js +87 -0
  21. package/{eval_test.js → examples/eval.js} +0 -0
  22. package/examples/simple.js +9 -2
  23. package/examples/sort.js +17 -0
  24. package/generate_commands.js +0 -1
  25. package/index.js +467 -214
  26. package/lib/commands.js +22 -1
  27. package/lib/parser/hiredis.js +15 -10
  28. package/lib/parser/javascript.js +14 -13
  29. package/lib/queue.js +5 -2
  30. package/lib/util.js +10 -5
  31. package/mem.js +11 -0
  32. package/multi_bench.js +197 -107
  33. package/package.json +6 -13
  34. package/test.js +465 -95
  35. package/simple_test.js +0 -3
  36. package/tests/test_start_stop.js +0 -17
package/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  /*global Buffer require exports console setTimeout */
2
2
 
3
3
  var net = require("net"),
4
- util = require("./lib/util").util,
5
- Queue = require("./lib/queue").Queue,
4
+ util = require("./lib/util"),
5
+ Queue = require("./lib/queue"),
6
6
  to_array = require("./lib/to_array"),
7
7
  events = require("events"),
8
+ crypto = require("crypto"),
8
9
  parsers = [], commands,
10
+ connection_id = 0,
9
11
  default_port = 6379,
10
12
  default_host = "127.0.0.1";
11
13
 
@@ -18,7 +20,7 @@ try {
18
20
  parsers.push(require("./lib/parser/hiredis"));
19
21
  } catch (err) {
20
22
  if (exports.debug_mode) {
21
- console.log("hiredis parser not installed.");
23
+ console.warn("hiredis parser not installed.");
22
24
  }
23
25
  }
24
26
 
@@ -26,64 +28,48 @@ parsers.push(require("./lib/parser/javascript"));
26
28
 
27
29
  function RedisClient(stream, options) {
28
30
  this.stream = stream;
29
- this.options = options || {};
31
+ this.options = options = options || {};
30
32
 
33
+ this.connection_id = ++connection_id;
31
34
  this.connected = false;
32
35
  this.ready = false;
33
36
  this.connections = 0;
34
- this.attempts = 1;
37
+ if (this.options.socket_nodelay === undefined) {
38
+ this.options.socket_nodelay = true;
39
+ }
35
40
  this.should_buffer = false;
36
41
  this.command_queue_high_water = this.options.command_queue_high_water || 1000;
37
42
  this.command_queue_low_water = this.options.command_queue_low_water || 0;
43
+ this.max_attempts = null;
44
+ if (options.max_attempts && !isNaN(options.max_attempts) && options.max_attempts > 0) {
45
+ this.max_attempts = +options.max_attempts;
46
+ }
38
47
  this.command_queue = new Queue(); // holds sent commands to de-pipeline them
39
48
  this.offline_queue = new Queue(); // holds commands issued but not able to be sent
40
49
  this.commands_sent = 0;
41
- this.retry_delay = 250; // inital reconnection delay
42
- this.current_retry_delay = this.retry_delay;
43
- this.retry_backoff = 1.7; // each retry waits current delay * retry_backoff
44
- this.subscriptions = false;
50
+ this.connect_timeout = false;
51
+ if (options.connect_timeout && !isNaN(options.connect_timeout) && options.connect_timeout > 0) {
52
+ this.connect_timeout = +options.connect_timeout;
53
+ }
54
+
55
+ this.enable_offline_queue = true;
56
+ if (typeof this.options.enable_offline_queue === "boolean") {
57
+ this.enable_offline_queue = this.options.enable_offline_queue;
58
+ }
59
+
60
+ this.initialize_retry_vars();
61
+ this.pub_sub_mode = false;
62
+ this.subscription_set = {};
45
63
  this.monitoring = false;
46
64
  this.closing = false;
47
65
  this.server_info = {};
48
66
  this.auth_pass = null;
67
+ this.parser_module = null;
68
+ this.selected_db = null; // save the selected db here, used when reconnecting
49
69
 
50
- var parser_module, self = this;
51
-
52
- if (self.options.parser) {
53
- if (! parsers.some(function (parser) {
54
- if (parser.name === self.options.parser) {
55
- parser_module = parser;
56
- if (exports.debug_mode) {
57
- console.log("Using parser module: " + parser_module.name);
58
- }
59
- return true;
60
- }
61
- })) {
62
- throw new Error("Couldn't find named parser " + self.options.parser + " on this system");
63
- }
64
- } else {
65
- if (exports.debug_mode) {
66
- console.log("Using default parser module: " + parsers[0].name);
67
- }
68
- parser_module = parsers[0];
69
- }
70
-
71
- parser_module.debug_mode = exports.debug_mode;
72
- this.reply_parser = new parser_module.Parser({
73
- return_buffers: self.options.return_buffers || false
74
- });
70
+ this.old_state = null;
75
71
 
76
- // "reply error" is an error sent back by Redis
77
- this.reply_parser.on("reply error", function (reply) {
78
- self.return_error(new Error(reply));
79
- });
80
- this.reply_parser.on("reply", function (reply) {
81
- self.return_reply(reply);
82
- });
83
- // "error" is bad. Somehow the parser got confused. It'll try to reset and continue.
84
- this.reply_parser.on("error", function (err) {
85
- self.emit("error", new Error("Redis reply parser error: " + err.stack));
86
- });
72
+ var self = this;
87
73
 
88
74
  this.stream.on("connect", function () {
89
75
  self.on_connect();
@@ -94,36 +80,7 @@ function RedisClient(stream, options) {
94
80
  });
95
81
 
96
82
  this.stream.on("error", function (msg) {
97
- if (this.closing) {
98
- return;
99
- }
100
-
101
- var message = "Redis connection to " + self.host + ":" + self.port + " failed - " + msg.message;
102
-
103
- if (exports.debug_mode) {
104
- console.warn(message);
105
- }
106
- self.offline_queue.forEach(function (args) {
107
- if (typeof args[2] === "function") {
108
- args[2](message);
109
- }
110
- });
111
- self.offline_queue = new Queue();
112
-
113
- self.command_queue.forEach(function (args) {
114
- if (typeof args[2] === "function") {
115
- args[2](message);
116
- }
117
- });
118
- self.command_queue = new Queue();
119
-
120
- self.connected = false;
121
- self.ready = false;
122
-
123
- self.emit("error", new Error(message));
124
- // "error" events get turned into exceptions if they aren't listened for. If the user handled this error
125
- // then we should try to reconnect.
126
- self.connection_gone("error");
83
+ self.on_error(msg.message);
127
84
  });
128
85
 
129
86
  this.stream.on("close", function () {
@@ -144,11 +101,62 @@ function RedisClient(stream, options) {
144
101
  util.inherits(RedisClient, events.EventEmitter);
145
102
  exports.RedisClient = RedisClient;
146
103
 
104
+ RedisClient.prototype.initialize_retry_vars = function () {
105
+ this.retry_timer = null;
106
+ this.retry_totaltime = 0;
107
+ this.retry_delay = 150;
108
+ this.retry_backoff = 1.7;
109
+ this.attempts = 1;
110
+ };
111
+
112
+ // flush offline_queue and command_queue, erroring any items with a callback first
113
+ RedisClient.prototype.flush_and_error = function (message) {
114
+ var command_obj;
115
+ while (this.offline_queue.length > 0) {
116
+ command_obj = this.offline_queue.shift();
117
+ if (typeof command_obj.callback === "function") {
118
+ command_obj.callback(message);
119
+ }
120
+ }
121
+ this.offline_queue = new Queue();
122
+
123
+ while (this.command_queue.length > 0) {
124
+ command_obj = this.command_queue.shift();
125
+ if (typeof command_obj.callback === "function") {
126
+ command_obj.callback(message);
127
+ }
128
+ }
129
+ this.command_queue = new Queue();
130
+ };
131
+
132
+ RedisClient.prototype.on_error = function (msg) {
133
+ var message = "Redis connection to " + this.host + ":" + this.port + " failed - " + msg,
134
+ self = this, command_obj;
135
+
136
+ if (this.closing) {
137
+ return;
138
+ }
139
+
140
+ if (exports.debug_mode) {
141
+ console.warn(message);
142
+ }
143
+
144
+ this.flush_and_error(message);
145
+
146
+ this.connected = false;
147
+ this.ready = false;
148
+
149
+ this.emit("error", new Error(message));
150
+ // "error" events get turned into exceptions if they aren't listened for. If the user handled this error
151
+ // then we should try to reconnect.
152
+ this.connection_gone("error");
153
+ };
154
+
147
155
  RedisClient.prototype.do_auth = function () {
148
156
  var self = this;
149
157
 
150
158
  if (exports.debug_mode) {
151
- console.log("Sending auth to " + self.host + ":" + self.port + " fd " + self.stream.fd);
159
+ console.log("Sending auth to " + self.host + ":" + self.port + " id " + self.connection_id);
152
160
  }
153
161
  self.send_anyway = true;
154
162
  self.send_command("auth", [this.auth_pass], function (err, res) {
@@ -161,14 +169,14 @@ RedisClient.prototype.do_auth = function () {
161
169
  }, 2000); // TODO - magic number alert
162
170
  return;
163
171
  } else {
164
- return self.emit("error", "Auth error: " + err);
172
+ return self.emit("error", new Error("Auth error: " + err.message));
165
173
  }
166
174
  }
167
175
  if (res.toString() !== "OK") {
168
- return self.emit("error", "Auth failed: " + res.toString());
176
+ return self.emit("error", new Error("Auth failed: " + res.toString()));
169
177
  }
170
178
  if (exports.debug_mode) {
171
- console.log("Auth succeeded " + self.host + ":" + self.port + " fd " + self.stream.fd);
179
+ console.log("Auth succeeded " + self.host + ":" + self.port + " id " + self.connection_id);
172
180
  }
173
181
  if (self.auth_callback) {
174
182
  self.auth_callback(err, res);
@@ -178,8 +186,7 @@ RedisClient.prototype.do_auth = function () {
178
186
  // now we are really connected
179
187
  self.emit("connect");
180
188
  if (self.options.no_ready_check) {
181
- self.ready = true;
182
- self.send_offline_queue();
189
+ self.on_ready();
183
190
  } else {
184
191
  self.ready_check();
185
192
  }
@@ -189,7 +196,7 @@ RedisClient.prototype.do_auth = function () {
189
196
 
190
197
  RedisClient.prototype.on_connect = function () {
191
198
  if (exports.debug_mode) {
192
- console.log("Stream connected " + this.host + ":" + this.port + " fd " + this.stream.fd);
199
+ console.log("Stream connected " + this.host + ":" + this.port + " id " + this.connection_id);
193
200
  }
194
201
  var self = this;
195
202
 
@@ -199,83 +206,172 @@ RedisClient.prototype.on_connect = function () {
199
206
  this.connections += 1;
200
207
  this.command_queue = new Queue();
201
208
  this.emitted_end = false;
202
- this.retry_timer = null;
203
- this.current_retry_delay = this.retry_time;
204
- this.stream.setNoDelay();
209
+ this.initialize_retry_vars();
210
+ if (this.options.socket_nodelay) {
211
+ this.stream.setNoDelay();
212
+ }
205
213
  this.stream.setTimeout(0);
206
214
 
215
+ this.init_parser();
216
+
207
217
  if (this.auth_pass) {
208
218
  this.do_auth();
209
219
  } else {
210
220
  this.emit("connect");
211
221
 
212
222
  if (this.options.no_ready_check) {
213
- this.ready = true;
214
- this.send_offline_queue();
223
+ this.on_ready();
215
224
  } else {
216
225
  this.ready_check();
217
226
  }
218
227
  }
219
228
  };
220
229
 
221
- RedisClient.prototype.ready_check = function () {
230
+ RedisClient.prototype.init_parser = function () {
222
231
  var self = this;
223
232
 
224
- function send_info_cmd() {
233
+ if (this.options.parser) {
234
+ if (! parsers.some(function (parser) {
235
+ if (parser.name === self.options.parser) {
236
+ self.parser_module = parser;
237
+ if (exports.debug_mode) {
238
+ console.log("Using parser module: " + self.parser_module.name);
239
+ }
240
+ return true;
241
+ }
242
+ })) {
243
+ throw new Error("Couldn't find named parser " + self.options.parser + " on this system");
244
+ }
245
+ } else {
225
246
  if (exports.debug_mode) {
226
- console.log("checking server ready state...");
247
+ console.log("Using default parser module: " + parsers[0].name);
227
248
  }
249
+ this.parser_module = parsers[0];
250
+ }
228
251
 
229
- self.send_anyway = true; // secret flag to send_command to send something even if not "ready"
230
- self.info(function (err, res) {
231
- if (err) {
232
- return self.emit("error", "Ready check failed: " + err);
233
- }
252
+ this.parser_module.debug_mode = exports.debug_mode;
234
253
 
235
- var lines = res.toString().split("\r\n"), obj = {}, retry_time;
254
+ // return_buffers sends back Buffers from parser to callback. detect_buffers sends back Buffers from parser, but
255
+ // converts to Strings if the input arguments are not Buffers.
256
+ this.reply_parser = new this.parser_module.Parser({
257
+ return_buffers: self.options.return_buffers || self.options.detect_buffers || false
258
+ });
236
259
 
237
- lines.forEach(function (line) {
238
- var parts = line.split(':');
239
- if (parts[1]) {
240
- obj[parts[0]] = parts[1];
241
- }
242
- });
260
+ // "reply error" is an error sent back by Redis
261
+ this.reply_parser.on("reply error", function (reply) {
262
+ self.return_error(new Error(reply));
263
+ });
264
+ this.reply_parser.on("reply", function (reply) {
265
+ self.return_reply(reply);
266
+ });
267
+ // "error" is bad. Somehow the parser got confused. It'll try to reset and continue.
268
+ this.reply_parser.on("error", function (err) {
269
+ self.emit("error", new Error("Redis reply parser error: " + err.stack));
270
+ });
271
+ };
243
272
 
244
- obj.versions = [];
245
- obj.redis_version.split('.').forEach(function (num) {
246
- obj.versions.push(+num);
247
- });
273
+ RedisClient.prototype.on_ready = function () {
274
+ var self = this;
248
275
 
249
- // expose info key/vals to users
250
- self.server_info = obj;
276
+ this.ready = true;
251
277
 
252
- if (!obj.loading || (obj.loading && obj.loading === "0")) {
253
- if (exports.debug_mode) {
254
- console.log("Redis server ready.");
255
- }
256
- self.ready = true;
278
+ if (this.old_state !== null) {
279
+ this.monitoring = this.old_state.monitoring;
280
+ this.pub_sub_mode = this.old_state.pub_sub_mode;
281
+ this.selected_db = this.old_state.selected_db;
282
+ this.old_state = null;
283
+ }
257
284
 
258
- self.send_offline_queue();
285
+ // magically restore any modal commands from a previous connection
286
+ if (this.selected_db !== null) {
287
+ this.send_command('select', [this.selected_db]);
288
+ }
289
+ if (this.pub_sub_mode === true) {
290
+ // only emit "ready" when all subscriptions were made again
291
+ var callback_count = 0;
292
+ var callback = function() {
293
+ callback_count--;
294
+ if (callback_count == 0) {
259
295
  self.emit("ready");
260
- } else {
261
- retry_time = obj.loading_eta_seconds * 1000;
262
- if (retry_time > 1000) {
263
- retry_time = 1000;
264
- }
265
- if (exports.debug_mode) {
266
- console.log("Redis server still loading, trying again in " + retry_time);
267
- }
268
- setTimeout(send_info_cmd, retry_time);
269
296
  }
297
+ }
298
+ Object.keys(this.subscription_set).forEach(function (key) {
299
+ var parts = key.split(" ");
300
+ if (exports.debug_mode) {
301
+ console.warn("sending pub/sub on_ready " + parts[0] + ", " + parts[1]);
302
+ }
303
+ callback_count++;
304
+ self.send_command(parts[0] + "scribe", [parts[1]], callback);
270
305
  });
271
- self.send_anyway = false;
306
+ return;
307
+ } else if (this.monitoring) {
308
+ this.send_command("monitor");
309
+ } else {
310
+ this.send_offline_queue();
311
+ }
312
+ this.emit("ready");
313
+ };
314
+
315
+ RedisClient.prototype.on_info_cmd = function (err, res) {
316
+ var self = this, obj = {}, lines, retry_time;
317
+
318
+ if (err) {
319
+ return self.emit("error", new Error("Ready check failed: " + err.message));
272
320
  }
273
321
 
274
- send_info_cmd();
322
+ lines = res.toString().split("\r\n");
323
+
324
+ lines.forEach(function (line) {
325
+ var parts = line.split(':');
326
+ if (parts[1]) {
327
+ obj[parts[0]] = parts[1];
328
+ }
329
+ });
330
+
331
+ obj.versions = [];
332
+ obj.redis_version.split('.').forEach(function (num) {
333
+ obj.versions.push(+num);
334
+ });
335
+
336
+ // expose info key/vals to users
337
+ this.server_info = obj;
338
+
339
+ if (!obj.loading || (obj.loading && obj.loading === "0")) {
340
+ if (exports.debug_mode) {
341
+ console.log("Redis server ready.");
342
+ }
343
+ this.on_ready();
344
+ } else {
345
+ retry_time = obj.loading_eta_seconds * 1000;
346
+ if (retry_time > 1000) {
347
+ retry_time = 1000;
348
+ }
349
+ if (exports.debug_mode) {
350
+ console.log("Redis server still loading, trying again in " + retry_time);
351
+ }
352
+ setTimeout(function () {
353
+ self.ready_check();
354
+ }, retry_time);
355
+ }
356
+ };
357
+
358
+ RedisClient.prototype.ready_check = function () {
359
+ var self = this;
360
+
361
+ if (exports.debug_mode) {
362
+ console.log("checking server ready state...");
363
+ }
364
+
365
+ this.send_anyway = true; // secret flag to send_command to send something even if not "ready"
366
+ this.info(function (err, res) {
367
+ self.on_info_cmd(err, res);
368
+ });
369
+ this.send_anyway = false;
275
370
  };
276
371
 
277
372
  RedisClient.prototype.send_offline_queue = function () {
278
373
  var command_obj, buffered_writes = 0;
374
+
279
375
  while (this.offline_queue.length > 0) {
280
376
  command_obj = this.offline_queue.shift();
281
377
  if (exports.debug_mode) {
@@ -293,23 +389,30 @@ RedisClient.prototype.send_offline_queue = function () {
293
389
  };
294
390
 
295
391
  RedisClient.prototype.connection_gone = function (why) {
296
- var self = this;
392
+ var self = this, message;
297
393
 
298
394
  // If a retry is already in progress, just let that happen
299
395
  if (this.retry_timer) {
300
396
  return;
301
397
  }
302
398
 
303
- // Note that this may trigger another "close" or "end" event
304
- this.stream.destroy();
305
-
306
399
  if (exports.debug_mode) {
307
400
  console.warn("Redis connection is gone from " + why + " event.");
308
401
  }
309
402
  this.connected = false;
310
403
  this.ready = false;
311
- this.subscriptions = false;
312
- this.monitoring = false;
404
+
405
+ if (this.old_state === null) {
406
+ var state = {
407
+ monitoring: this.monitoring,
408
+ pub_sub_mode: this.pub_sub_mode,
409
+ selected_db: this.selected_db
410
+ };
411
+ this.old_state = state;
412
+ this.monitoring = false;
413
+ this.pub_sub_mode = false;
414
+ this.selected_db = null;
415
+ }
313
416
 
314
417
  // since we are collapsing end and close, users don't expect to be called twice
315
418
  if (! this.emitted_end) {
@@ -317,41 +420,58 @@ RedisClient.prototype.connection_gone = function (why) {
317
420
  this.emitted_end = true;
318
421
  }
319
422
 
320
- this.command_queue.forEach(function (args) {
321
- if (typeof args[2] === "function") {
322
- args[2]("Server connection closed");
323
- }
324
- });
325
- this.command_queue = new Queue();
423
+ this.flush_and_error("Redis connection gone from " + why + " event.");
326
424
 
327
425
  // If this is a requested shutdown, then don't retry
328
426
  if (this.closing) {
329
427
  this.retry_timer = null;
428
+ if (exports.debug_mode) {
429
+ console.warn("connection ended from quit command, not retrying.");
430
+ }
330
431
  return;
331
432
  }
332
433
 
333
- this.current_retry_delay = this.retry_delay * this.retry_backoff;
434
+ this.retry_delay = Math.floor(this.retry_delay * this.retry_backoff);
334
435
 
335
436
  if (exports.debug_mode) {
336
437
  console.log("Retry connection in " + this.current_retry_delay + " ms");
337
438
  }
439
+
440
+ if (this.max_attempts && this.attempts >= this.max_attempts) {
441
+ this.retry_timer = null;
442
+ // TODO - some people need a "Redis is Broken mode" for future commands that errors immediately, and others
443
+ // want the program to exit. Right now, we just log, which doesn't really help in either case.
444
+ console.error("node_redis: Couldn't get Redis connection after " + this.max_attempts + " attempts.");
445
+ return;
446
+ }
447
+
338
448
  this.attempts += 1;
339
449
  this.emit("reconnecting", {
340
- delay: this.current_retry_delay,
341
- attempt: this.attempts
450
+ delay: self.retry_delay,
451
+ attempt: self.attempts
342
452
  });
343
453
  this.retry_timer = setTimeout(function () {
344
454
  if (exports.debug_mode) {
345
455
  console.log("Retrying connection...");
346
456
  }
457
+
458
+ self.retry_totaltime += self.current_retry_delay;
459
+
460
+ if (self.connect_timeout && self.retry_totaltime >= self.connect_timeout) {
461
+ self.retry_timer = null;
462
+ // TODO - engage Redis is Broken mode for future commands, or whatever
463
+ console.error("node_redis: Couldn't get Redis connection after " + self.retry_totaltime + "ms.");
464
+ return;
465
+ }
466
+
347
467
  self.stream.connect(self.port, self.host);
348
468
  self.retry_timer = null;
349
- }, this.current_retry_delay);
469
+ }, this.retry_delay);
350
470
  };
351
471
 
352
472
  RedisClient.prototype.on_data = function (data) {
353
473
  if (exports.debug_mode) {
354
- console.log("net read " + this.host + ":" + this.port + " fd " + this.stream.fd + ": " + data.toString());
474
+ console.log("net read " + this.host + ":" + this.port + " id " + this.connection_id + ": " + data.toString());
355
475
  }
356
476
 
357
477
  try {
@@ -368,7 +488,7 @@ RedisClient.prototype.on_data = function (data) {
368
488
  RedisClient.prototype.return_error = function (err) {
369
489
  var command_obj = this.command_queue.shift(), queue_len = this.command_queue.getLength();
370
490
 
371
- if (this.subscriptions === false && queue_len === 0) {
491
+ if (this.pub_sub_mode === false && queue_len === 0) {
372
492
  this.emit("idle");
373
493
  this.command_queue = new Queue();
374
494
  }
@@ -395,11 +515,59 @@ RedisClient.prototype.return_error = function (err) {
395
515
  }
396
516
  };
397
517
 
518
+ // if a callback throws an exception, re-throw it on a new stack so the parser can keep going.
519
+ // put this try/catch in its own function because V8 doesn't optimize this well yet.
520
+ function try_callback(callback, reply) {
521
+ try {
522
+ callback(null, reply);
523
+ } catch (err) {
524
+ process.nextTick(function () {
525
+ throw err;
526
+ });
527
+ }
528
+ }
529
+
530
+ // hgetall converts its replies to an Object. If the reply is empty, null is returned.
531
+ function reply_to_object(reply) {
532
+ var obj = {}, j, jl, key, val;
533
+
534
+ if (reply.length === 0) {
535
+ return null;
536
+ }
537
+
538
+ for (j = 0, jl = reply.length; j < jl; j += 2) {
539
+ key = reply[j].toString();
540
+ val = reply[j + 1];
541
+ obj[key] = val;
542
+ }
543
+
544
+ return obj;
545
+ }
546
+
547
+ function reply_to_strings(reply) {
548
+ var i;
549
+
550
+ if (Buffer.isBuffer(reply)) {
551
+ return reply.toString();
552
+ }
553
+
554
+ if (Array.isArray(reply)) {
555
+ for (i = 0; i < reply.length; i++) {
556
+ reply[i] = reply[i].toString();
557
+ }
558
+ return reply;
559
+ }
560
+
561
+ return reply;
562
+ }
563
+
398
564
  RedisClient.prototype.return_reply = function (reply) {
399
- var command_obj = this.command_queue.shift(),
400
- obj, i, len, key, val, type, timestamp, args, queue_len = this.command_queue.getLength();
565
+ var command_obj, obj, i, len, type, timestamp, argindex, args, queue_len;
566
+
567
+ command_obj = this.command_queue.shift(),
568
+ queue_len = this.command_queue.getLength();
401
569
 
402
- if (this.subscriptions === false && queue_len === 0) {
570
+ if (this.pub_sub_mode === false && queue_len === 0) {
403
571
  this.emit("idle");
404
572
  this.command_queue = new Queue(); // explicitly reclaim storage from old Queue
405
573
  }
@@ -410,29 +578,22 @@ RedisClient.prototype.return_reply = function (reply) {
410
578
 
411
579
  if (command_obj && !command_obj.sub_command) {
412
580
  if (typeof command_obj.callback === "function") {
413
- // HGETALL special case replies with keyed Buffers
414
- if (reply && 'hgetall' === command_obj.command.toLowerCase()) {
415
- obj = {};
416
- for (i = 0, len = reply.length; i < len; i += 2) {
417
- key = reply[i].toString();
418
- val = reply[i + 1];
419
- obj[key] = val;
420
- }
421
- reply = obj;
581
+ if (this.options.detect_buffers && command_obj.buffer_args === false) {
582
+ // If detect_buffers option was specified, then the reply from the parser will be Buffers.
583
+ // If this command did not use Buffer arguments, then convert the reply to Strings here.
584
+ reply = reply_to_strings(reply);
422
585
  }
423
586
 
424
- try {
425
- command_obj.callback(null, reply);
426
- } catch (err) {
427
- // if a callback throws an exception, re-throw it on a new stack so the parser can keep going
428
- process.nextTick(function () {
429
- throw err;
430
- });
587
+ // TODO - confusing and error-prone that hgetall is special cased in two places
588
+ if (reply && 'hgetall' === command_obj.command.toLowerCase()) {
589
+ reply = reply_to_object(reply);
431
590
  }
591
+
592
+ try_callback(command_obj.callback, reply);
432
593
  } else if (exports.debug_mode) {
433
594
  console.log("no callback for reply: " + (reply && reply.toString && reply.toString()));
434
595
  }
435
- } else if (this.subscriptions || (command_obj && command_obj.sub_command)) {
596
+ } else if (this.pub_sub_mode || (command_obj && command_obj.sub_command)) {
436
597
  if (Array.isArray(reply)) {
437
598
  type = reply[0].toString();
438
599
 
@@ -442,10 +603,17 @@ RedisClient.prototype.return_reply = function (reply) {
442
603
  this.emit("pmessage", reply[1].toString(), reply[2].toString(), reply[3]); // pattern, channel, message
443
604
  } else if (type === "subscribe" || type === "unsubscribe" || type === "psubscribe" || type === "punsubscribe") {
444
605
  if (reply[2] === 0) {
445
- this.subscriptions = false;
606
+ this.pub_sub_mode = false;
446
607
  if (this.debug_mode) {
447
608
  console.log("All subscriptions removed, exiting pub/sub mode");
448
609
  }
610
+ } else {
611
+ this.pub_sub_mode = true;
612
+ }
613
+ // subscribe commands take an optional callback and also emit an event, but only the first response is included in the callback
614
+ // TODO - document this or fix it so it works in a more obvious way
615
+ if (command_obj && typeof command_obj.callback === "function") {
616
+ try_callback(command_obj.callback, reply[1].toString());
449
617
  }
450
618
  this.emit(type, reply[1].toString(), reply[2]); // channel, count
451
619
  } else {
@@ -457,9 +625,9 @@ RedisClient.prototype.return_reply = function (reply) {
457
625
  } else if (this.monitoring) {
458
626
  len = reply.indexOf(" ");
459
627
  timestamp = reply.slice(0, len);
460
- // TODO - this de-quoting doesn't work correctly if you put JSON strings in your values.
461
- args = reply.slice(len + 1).match(/"[^"]+"/g).map(function (elem) {
462
- return elem.replace(/"/g, "");
628
+ argindex = reply.indexOf('"');
629
+ args = reply.slice(argindex + 1, -1).split('" "').map(function (elem) {
630
+ return elem.replace(/\\"/g, '"');
463
631
  });
464
632
  this.emit("monitor", timestamp, args);
465
633
  } else {
@@ -467,16 +635,18 @@ RedisClient.prototype.return_reply = function (reply) {
467
635
  }
468
636
  };
469
637
 
470
- // This Command constructor is ever so slightly faster than using an object literal
471
- function Command(command, args, sub_command, callback) {
638
+ // This Command constructor is ever so slightly faster than using an object literal, but more importantly, using
639
+ // a named constructor helps it show up meaningfully in the V8 CPU profiler and in heap snapshots.
640
+ function Command(command, args, sub_command, buffer_args, callback) {
472
641
  this.command = command;
473
642
  this.args = args;
474
643
  this.sub_command = sub_command;
644
+ this.buffer_args = buffer_args;
475
645
  this.callback = callback;
476
646
  }
477
647
 
478
648
  RedisClient.prototype.send_command = function (command, args, callback) {
479
- var arg, this_args, command_obj, i, il, elem_count, stream = this.stream, buffer_args, command_str = "", buffered_writes = 0;
649
+ var arg, this_args, command_obj, i, il, elem_count, buffer_args, stream = this.stream, command_str = "", buffered_writes = 0, last_arg_type;
480
650
 
481
651
  if (typeof command !== "string") {
482
652
  throw new Error("First argument to send_command must be the command name string, not " + typeof command);
@@ -493,9 +663,11 @@ RedisClient.prototype.send_command = function (command, args, callback) {
493
663
  // send_command(command, [arg1, arg2, cb]);
494
664
  // client.command(arg1, arg2); (callback is optional)
495
665
  // send_command(command, [arg1, arg2]);
496
- if (typeof args[args.length - 1] === "function") {
497
- callback = args[args.length - 1];
498
- args.length -= 1;
666
+ // client.command(arg1, arg2, undefined); (callback is undefined)
667
+ // send_command(command, [arg1, arg2, undefined]);
668
+ last_arg_type = typeof args[args.length - 1];
669
+ if (last_arg_type === "function" || last_arg_type === "undefined") {
670
+ callback = args.pop();
499
671
  }
500
672
  } else {
501
673
  throw new Error("send_command: last argument must be a callback or undefined");
@@ -504,63 +676,67 @@ RedisClient.prototype.send_command = function (command, args, callback) {
504
676
  throw new Error("send_command: second argument must be an array");
505
677
  }
506
678
 
507
- // if the last argument is an array, expand it out. This allows commands like this:
508
- // client.command(arg1, [arg2, arg3, arg4], cb);
509
- // and converts to:
510
- // client.command(arg1, arg2, arg3, arg4, cb);
511
- // which is convenient for some things like sadd
512
- if (Array.isArray(args[args.length - 1])) {
679
+ // if the last argument is an array and command is sadd, expand it out:
680
+ // client.sadd(arg1, [arg2, arg3, arg4], cb);
681
+ // converts to:
682
+ // client.sadd(arg1, arg2, arg3, arg4, cb);
683
+ if ((command === 'sadd' || command === 'SADD') && args.length > 0 && Array.isArray(args[args.length - 1])) {
513
684
  args = args.slice(0, -1).concat(args[args.length - 1]);
514
685
  }
515
686
 
516
- command_obj = new Command(command, args, false, callback);
687
+ buffer_args = false;
688
+ for (i = 0, il = args.length, arg; i < il; i += 1) {
689
+ if (Buffer.isBuffer(args[i])) {
690
+ buffer_args = true;
691
+ }
692
+ }
693
+
694
+ command_obj = new Command(command, args, false, buffer_args, callback);
517
695
 
518
696
  if ((!this.ready && !this.send_anyway) || !stream.writable) {
519
697
  if (exports.debug_mode) {
520
698
  if (!stream.writable) {
521
699
  console.log("send command: stream is not writeable.");
522
700
  }
523
-
524
- console.log("Queueing " + command + " for next server connection.");
525
701
  }
526
- this.offline_queue.push(command_obj);
527
- this.should_buffer = true;
702
+
703
+ if (this.enable_offline_queue) {
704
+ if (exports.debug_mode) {
705
+ console.log("Queueing " + command + " for next server connection.");
706
+ }
707
+ this.offline_queue.push(command_obj);
708
+ this.should_buffer = true;
709
+ } else {
710
+ var not_writeable_error = new Error('send_command: stream not writeable. enable_offline_queue is false');
711
+ if (command_obj.callback) {
712
+ command_obj.callback(not_writeable_error);
713
+ } else {
714
+ throw not_writeable_error;
715
+ }
716
+ }
717
+
528
718
  return false;
529
719
  }
530
720
 
531
721
  if (command === "subscribe" || command === "psubscribe" || command === "unsubscribe" || command === "punsubscribe") {
532
- if (this.subscriptions === false && exports.debug_mode) {
533
- console.log("Entering pub/sub mode from " + command);
534
- }
535
- command_obj.sub_command = true;
536
- this.subscriptions = true;
722
+ this.pub_sub_command(command_obj);
537
723
  } else if (command === "monitor") {
538
724
  this.monitoring = true;
539
725
  } else if (command === "quit") {
540
726
  this.closing = true;
541
- } else if (this.subscriptions === true) {
727
+ } else if (this.pub_sub_mode === true) {
542
728
  throw new Error("Connection in pub/sub mode, only pub/sub commands may be used");
543
729
  }
544
730
  this.command_queue.push(command_obj);
545
731
  this.commands_sent += 1;
546
732
 
547
- elem_count = 1;
548
- buffer_args = false;
549
-
550
- elem_count += args.length;
733
+ elem_count = args.length + 1;
551
734
 
552
- // Always use "Multi bulk commands", but if passed any Buffer args, then do multiple writes, one for each arg
735
+ // Always use "Multi bulk commands", but if passed any Buffer args, then do multiple writes, one for each arg.
553
736
  // This means that using Buffers in commands is going to be slower, so use Strings if you don't already have a Buffer.
554
- // Also, why am I putting user documentation in the library source code?
555
737
 
556
738
  command_str = "*" + elem_count + "\r\n$" + command.length + "\r\n" + command + "\r\n";
557
739
 
558
- for (i = 0, il = args.length, arg; i < il; i += 1) {
559
- if (Buffer.isBuffer(args[i])) {
560
- buffer_args = true;
561
- }
562
- }
563
-
564
740
  if (! buffer_args) { // Build up a string and send entire command in one write
565
741
  for (i = 0, il = args.length, arg; i < il; i += 1) {
566
742
  arg = args[i];
@@ -570,7 +746,7 @@ RedisClient.prototype.send_command = function (command, args, callback) {
570
746
  command_str += "$" + Buffer.byteLength(arg) + "\r\n" + arg + "\r\n";
571
747
  }
572
748
  if (exports.debug_mode) {
573
- console.log("send " + this.host + ":" + this.port + " fd " + this.stream.fd + ": " + command_str);
749
+ console.log("send " + this.host + ":" + this.port + " id " + this.connection_id + ": " + command_str);
574
750
  }
575
751
  buffered_writes += !stream.write(command_str);
576
752
  } else {
@@ -616,6 +792,38 @@ RedisClient.prototype.send_command = function (command, args, callback) {
616
792
  return !this.should_buffer;
617
793
  };
618
794
 
795
+ RedisClient.prototype.pub_sub_command = function (command_obj) {
796
+ var i, key, command, args;
797
+
798
+ if (this.pub_sub_mode === false && exports.debug_mode) {
799
+ console.log("Entering pub/sub mode from " + command_obj.command);
800
+ }
801
+ this.pub_sub_mode = true;
802
+ command_obj.sub_command = true;
803
+
804
+ command = command_obj.command;
805
+ args = command_obj.args;
806
+ if (command === "subscribe" || command === "psubscribe") {
807
+ if (command === "subscribe") {
808
+ key = "sub";
809
+ } else {
810
+ key = "psub";
811
+ }
812
+ for (i = 0; i < args.length; i++) {
813
+ this.subscription_set[key + " " + args[i]] = true;
814
+ }
815
+ } else {
816
+ if (command === "unsubscribe") {
817
+ key = "sub";
818
+ } else {
819
+ key = "psub";
820
+ }
821
+ for (i = 0; i < args.length; i++) {
822
+ delete this.subscription_set[key + " " + args[i]];
823
+ }
824
+ }
825
+ };
826
+
619
827
  RedisClient.prototype.end = function () {
620
828
  this.stream._events = {};
621
829
  this.connected = false;
@@ -636,7 +844,7 @@ exports.Multi = Multi;
636
844
  // take 2 arrays and return the union of their elements
637
845
  function set_union(seta, setb) {
638
846
  var obj = {};
639
-
847
+
640
848
  seta.forEach(function (val) {
641
849
  obj[val] = true;
642
850
  });
@@ -675,6 +883,21 @@ commands.forEach(function (command) {
675
883
  Multi.prototype[command.toUpperCase()] = Multi.prototype[command];
676
884
  });
677
885
 
886
+ // store db in this.select_db to restore it on reconnect
887
+ RedisClient.prototype.select = function (db, callback) {
888
+ var self = this;
889
+
890
+ this.send_command('select', [db], function (err, res) {
891
+ if (err === null) {
892
+ self.selected_db = db;
893
+ }
894
+ if (typeof(callback) === 'function') {
895
+ callback(err, res);
896
+ }
897
+ });
898
+ };
899
+ RedisClient.prototype.SELECT = RedisClient.prototype.select;
900
+
678
901
  // Stash auth for connect and reconnect. Send immediately if already connected.
679
902
  RedisClient.prototype.auth = function () {
680
903
  var args = to_array(arguments);
@@ -723,6 +946,11 @@ RedisClient.prototype.hmset = function (args, callback) {
723
946
  for (i = 0, il = tmp_keys.length; i < il ; i++) {
724
947
  key = tmp_keys[i];
725
948
  tmp_args.push(key);
949
+ if (typeof args[1][key] !== "string") {
950
+ var err = new Error("hmset expected value to be a string", key, ":", args[1][key]);
951
+ if (callback) return callback(err);
952
+ else throw err;
953
+ }
726
954
  tmp_args.push(args[1][key]);
727
955
  }
728
956
  args = tmp_args;
@@ -768,7 +996,7 @@ Multi.prototype.exec = function (callback) {
768
996
  if (args.length === 1 && Array.isArray(args[0])) {
769
997
  args = args[0];
770
998
  }
771
- if (command === 'hmset' && typeof args[1] === 'object') {
999
+ if (command.toLowerCase() === 'hmset' && typeof args[1] === 'object') {
772
1000
  obj = args.pop();
773
1001
  Object.keys(obj).forEach(function (key) {
774
1002
  args.push(key);
@@ -799,22 +1027,16 @@ Multi.prototype.exec = function (callback) {
799
1027
  }
800
1028
  }
801
1029
 
802
- var i, il, j, jl, reply, args, obj, key, val;
1030
+ var i, il, j, jl, reply, args;
803
1031
 
804
1032
  if (replies) {
805
1033
  for (i = 1, il = self.queue.length; i < il; i += 1) {
806
1034
  reply = replies[i - 1];
807
1035
  args = self.queue[i];
808
1036
 
809
- // Convert HGETALL reply to object
1037
+ // TODO - confusing and error-prone that hgetall is special cased in two places
810
1038
  if (reply && args[0].toLowerCase() === "hgetall") {
811
- obj = {};
812
- for (j = 0, jl = reply.length; j < jl; j += 2) {
813
- key = reply[j].toString();
814
- val = reply[j + 1];
815
- obj[key] = val;
816
- }
817
- replies[i - 1] = reply = obj;
1039
+ replies[i - 1] = reply = reply_to_object(reply);
818
1040
  }
819
1041
 
820
1042
  if (typeof args[args.length - 1] === "function") {
@@ -828,6 +1050,7 @@ Multi.prototype.exec = function (callback) {
828
1050
  }
829
1051
  });
830
1052
  };
1053
+ Multi.prototype.EXEC = Multi.prototype.exec;
831
1054
 
832
1055
  RedisClient.prototype.multi = function (args) {
833
1056
  return new Multi(this, args);
@@ -836,6 +1059,36 @@ RedisClient.prototype.MULTI = function (args) {
836
1059
  return new Multi(this, args);
837
1060
  };
838
1061
 
1062
+
1063
+ // stash original eval method
1064
+ var eval = RedisClient.prototype.eval;
1065
+ // hook eval with an attempt to evalsha for cached scripts
1066
+ RedisClient.prototype.eval =
1067
+ RedisClient.prototype.EVAL = function () {
1068
+ var self = this,
1069
+ args = to_array(arguments),
1070
+ callback;
1071
+
1072
+ if (typeof args[args.length - 1] === "function") {
1073
+ callback = args.pop();
1074
+ }
1075
+
1076
+ // replace script source with sha value
1077
+ var source = args[0];
1078
+ args[0] = crypto.createHash("sha1").update(source).digest("hex");
1079
+
1080
+ self.evalsha(args, function (err, reply) {
1081
+ if (err && /NOSCRIPT/.test(err.message)) {
1082
+ args[0] = source;
1083
+ eval.call(self, args, callback);
1084
+
1085
+ } else if (callback) {
1086
+ callback(err, reply);
1087
+ }
1088
+ });
1089
+ };
1090
+
1091
+
839
1092
  exports.createClient = function (port_arg, host_arg, options) {
840
1093
  var port = port_arg || default_port,
841
1094
  host = host_arg || default_host,