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.
- package/.npmignore +1 -0
- package/README.md +172 -48
- package/{tests → benches}/buffer_bench.js +0 -0
- package/benches/hiredis_parser.js +38 -0
- package/benches/re_sub_test.js +14 -0
- package/{tests → benches}/reconnect_test.js +4 -2
- package/{tests → benches}/stress/codec.js +0 -0
- package/{tests → benches}/stress/pubsub/pub.js +0 -0
- package/{tests → benches}/stress/pubsub/run +0 -0
- package/{tests → benches}/stress/pubsub/server.js +0 -0
- package/{tests → benches}/stress/rpushblpop/pub.js +0 -0
- package/{tests → benches}/stress/rpushblpop/run +0 -0
- package/{tests → benches}/stress/rpushblpop/server.js +0 -0
- package/{tests → benches}/stress/speed/00 +0 -0
- package/{tests → benches}/stress/speed/plot +0 -0
- package/{tests → benches}/stress/speed/size-rate.png +0 -0
- package/{tests → benches}/stress/speed/speed.js +0 -0
- package/{tests → benches}/sub_quit_test.js +0 -0
- package/changelog.md +35 -0
- package/diff_multi_bench_output.js +87 -0
- package/{eval_test.js → examples/eval.js} +0 -0
- package/examples/simple.js +9 -2
- package/examples/sort.js +17 -0
- package/generate_commands.js +0 -1
- package/index.js +467 -214
- package/lib/commands.js +22 -1
- package/lib/parser/hiredis.js +15 -10
- package/lib/parser/javascript.js +14 -13
- package/lib/queue.js +5 -2
- package/lib/util.js +10 -5
- package/mem.js +11 -0
- package/multi_bench.js +197 -107
- package/package.json +6 -13
- package/test.js +465 -95
- package/simple_test.js +0 -3
- 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")
|
|
5
|
-
Queue = require("./lib/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.
|
|
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.
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 + "
|
|
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 + "
|
|
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.
|
|
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 + "
|
|
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.
|
|
203
|
-
this.
|
|
204
|
-
|
|
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.
|
|
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.
|
|
230
|
+
RedisClient.prototype.init_parser = function () {
|
|
222
231
|
var self = this;
|
|
223
232
|
|
|
224
|
-
|
|
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("
|
|
247
|
+
console.log("Using default parser module: " + parsers[0].name);
|
|
227
248
|
}
|
|
249
|
+
this.parser_module = parsers[0];
|
|
250
|
+
}
|
|
228
251
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
obj.versions.push(+num);
|
|
247
|
-
});
|
|
273
|
+
RedisClient.prototype.on_ready = function () {
|
|
274
|
+
var self = this;
|
|
248
275
|
|
|
249
|
-
|
|
250
|
-
self.server_info = obj;
|
|
276
|
+
this.ready = true;
|
|
251
277
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
this.
|
|
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.
|
|
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.
|
|
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:
|
|
341
|
-
attempt:
|
|
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.
|
|
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 + "
|
|
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.
|
|
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
|
|
400
|
-
|
|
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.
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
461
|
-
args = reply.slice(
|
|
462
|
-
return elem.replace(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
508
|
-
// client.
|
|
509
|
-
//
|
|
510
|
-
// client.
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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 + "
|
|
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
|
|
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
|
-
//
|
|
1037
|
+
// TODO - confusing and error-prone that hgetall is special cased in two places
|
|
810
1038
|
if (reply && args[0].toLowerCase() === "hgetall") {
|
|
811
|
-
|
|
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,
|