Skip to content

Commit 97ae788

Browse files
author
Ruben Bridgewater
committed
Implement CLIENT REPLY ON|OFF|SKIP
1 parent 3038c90 commit 97ae788

File tree

5 files changed

+233
-24
lines changed

5 files changed

+233
-24
lines changed

index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ function RedisClient (options, stream) {
150150
this.times_connected = 0;
151151
this.options = options;
152152
this.buffers = options.return_buffers || options.detect_buffers;
153+
this.reply = 'ON'; // Returning replies is the default
153154
// Init parser
154155
this.reply_parser = create_parser(this, options);
155156
this.create_stream();
@@ -901,6 +902,22 @@ RedisClient.prototype.internal_send_command = function (command, args, callback,
901902
if (call_on_write) {
902903
call_on_write();
903904
}
905+
// Handle `CLIENT REPLY ON|OFF|SKIP`
906+
// This has to be checked after call_on_write
907+
if (this.reply === 'ON') {
908+
this.command_queue.push(command_obj);
909+
} else {
910+
// Do not expect a reply
911+
// Does this work in combination with the pub sub mode?
912+
if (callback) {
913+
utils.reply_in_order(this, callback, null, undefined, this.command_queue);
914+
}
915+
if (this.reply === 'SKIP') {
916+
this.reply = 'SKIP_ONE_MORE';
917+
} else if (this.reply === 'SKIP_ONE_MORE') {
918+
this.reply = 'ON';
919+
}
920+
}
904921
return !this.should_buffer;
905922
};
906923

lib/individualCommands.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,97 @@ Multi.prototype.auth = Multi.prototype.AUTH = function auth (pass, callback) {
226226
return this;
227227
};
228228

229+
RedisClient.prototype.client = RedisClient.prototype.CLIENT = function client () {
230+
var arr,
231+
len = arguments.length,
232+
callback,
233+
i = 0;
234+
if (Array.isArray(arguments[0])) {
235+
arr = arguments[0];
236+
callback = arguments[1];
237+
} else if (Array.isArray(arguments[1])) {
238+
if (len === 3) {
239+
callback = arguments[2];
240+
}
241+
len = arguments[1].length;
242+
arr = new Array(len + 1);
243+
arr[0] = arguments[0];
244+
for (; i < len; i += 1) {
245+
arr[i + 1] = arguments[1][i];
246+
}
247+
} else {
248+
len = arguments.length;
249+
// The later should not be the average use case
250+
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
251+
len--;
252+
callback = arguments[len];
253+
}
254+
arr = new Array(len);
255+
for (; i < len; i += 1) {
256+
arr[i] = arguments[i];
257+
}
258+
}
259+
var self = this;
260+
var call_on_write = undefined;
261+
// CLIENT REPLY ON|OFF|SKIP
262+
/* istanbul ignore next: TODO: Remove this as soon as Travis runs Redis 3.2 */
263+
if (arr.length === 2 && arr[0].toString().toUpperCase() === 'REPLY') {
264+
var reply_on_off = arr[1].toString().toUpperCase();
265+
if (reply_on_off === 'ON' || reply_on_off === 'OFF' || reply_on_off === 'SKIP') {
266+
call_on_write = function () {
267+
self.reply = reply_on_off;
268+
};
269+
}
270+
}
271+
return this.internal_send_command('client', arr, callback, call_on_write);
272+
};
273+
274+
Multi.prototype.client = Multi.prototype.CLIENT = function client () {
275+
var arr,
276+
len = arguments.length,
277+
callback,
278+
i = 0;
279+
if (Array.isArray(arguments[0])) {
280+
arr = arguments[0];
281+
callback = arguments[1];
282+
} else if (Array.isArray(arguments[1])) {
283+
if (len === 3) {
284+
callback = arguments[2];
285+
}
286+
len = arguments[1].length;
287+
arr = new Array(len + 1);
288+
arr[0] = arguments[0];
289+
for (; i < len; i += 1) {
290+
arr[i + 1] = arguments[1][i];
291+
}
292+
} else {
293+
len = arguments.length;
294+
// The later should not be the average use case
295+
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
296+
len--;
297+
callback = arguments[len];
298+
}
299+
arr = new Array(len);
300+
for (; i < len; i += 1) {
301+
arr[i] = arguments[i];
302+
}
303+
}
304+
var self = this._client;
305+
var call_on_write = undefined;
306+
// CLIENT REPLY ON|OFF|SKIP
307+
/* istanbul ignore next: TODO: Remove this as soon as Travis runs Redis 3.2 */
308+
if (arr.length === 2 && arr[0].toString().toUpperCase() === 'REPLY') {
309+
var reply_on_off = arr[1].toString().toUpperCase();
310+
if (reply_on_off === 'ON' || reply_on_off === 'OFF' || reply_on_off === 'SKIP') {
311+
call_on_write = function () {
312+
self.reply = reply_on_off;
313+
};
314+
}
315+
}
316+
this.queue.push(['client', arr, callback, call_on_write]);
317+
return this;
318+
};
319+
229320
RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function hmset () {
230321
var arr,
231322
len = arguments.length,

lib/utils.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,15 @@ function callbackOrEmit (self, callback, err, res) {
9090
}
9191
}
9292

93-
function replyInOrder (self, callback, err, res) {
94-
// The offline queue has to be checked first, as there might be commands in both queues at the same time
95-
var command_obj = self.offline_queue.peekBack() || self.command_queue.peekBack();
93+
function replyInOrder (self, callback, err, res, queue) {
94+
// If the queue is explicitly passed, use that, otherwise fall back to the offline queue first,
95+
// as there might be commands in both queues at the same time
96+
var command_obj;
97+
if (queue) {
98+
command_obj = queue.peekBack();
99+
} else {
100+
command_obj = self.offline_queue.peekBack() || self.command_queue.peekBack();
101+
}
96102
if (!command_obj) {
97103
process.nextTick(function () {
98104
callbackOrEmit(self, callback, err, res);

test/commands/client.spec.js

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,89 @@ describe("The 'client' method", function () {
3030
});
3131

3232
it("lists connected clients when invoked with multi's chaining syntax", function (done) {
33-
client.multi().client('list').exec(function (err, results) {
34-
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
35-
return done();
36-
});
33+
client.multi().client('list', helper.isType.string()).exec(helper.match(pattern, done));
3734
});
3835

3936
it('lists connected clients when invoked with array syntax on client', function (done) {
40-
client.multi().client(['list']).exec(function (err, results) {
41-
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
42-
return done();
43-
});
37+
client.multi().client(['list']).exec(helper.match(pattern, done));
4438
});
4539

4640
it("lists connected clients when invoked with multi's array syntax", function (done) {
4741
client.multi([
4842
['client', 'list']
49-
]).exec(function (err, results) {
50-
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
51-
return done();
43+
]).exec(helper.match(pattern, done));
44+
});
45+
});
46+
47+
describe('reply', function () {
48+
describe('as normal command', function () {
49+
it('on', function (done) {
50+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
51+
assert.strictEqual(client.reply, 'ON');
52+
client.client('reply', 'on', helper.isString('OK'));
53+
assert.strictEqual(client.reply, 'ON');
54+
client.set('foo', 'bar', done);
55+
});
56+
57+
it('off', function (done) {
58+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
59+
assert.strictEqual(client.reply, 'ON');
60+
client.client(new Buffer('REPLY'), 'OFF', helper.isUndefined());
61+
assert.strictEqual(client.reply, 'OFF');
62+
client.set('foo', 'bar', helper.isUndefined(done));
63+
});
64+
65+
it('skip', function (done) {
66+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
67+
assert.strictEqual(client.reply, 'ON');
68+
client.client('REPLY', new Buffer('SKIP'), helper.isUndefined());
69+
assert.strictEqual(client.reply, 'SKIP_ONE_MORE');
70+
client.set('foo', 'bar', helper.isUndefined());
71+
client.get('foo', helper.isString('bar', done));
72+
});
73+
});
74+
75+
describe('in a batch context', function () {
76+
it('on', function (done) {
77+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
78+
var batch = client.batch();
79+
assert.strictEqual(client.reply, 'ON');
80+
batch.client('reply', 'on', helper.isString('OK'));
81+
assert.strictEqual(client.reply, 'ON');
82+
batch.set('foo', 'bar');
83+
batch.exec(function (err, res) {
84+
assert.deepEqual(res, ['OK', 'OK']);
85+
done(err);
86+
});
87+
});
88+
89+
it('off', function (done) {
90+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
91+
var batch = client.batch();
92+
assert.strictEqual(client.reply, 'ON');
93+
batch.set('hello', 'world');
94+
batch.client(new Buffer('REPLY'), new Buffer('OFF'), helper.isUndefined());
95+
batch.set('foo', 'bar', helper.isUndefined());
96+
batch.exec(function (err, res) {
97+
assert.strictEqual(client.reply, 'OFF');
98+
assert.deepEqual(res, ['OK', undefined, undefined]);
99+
done(err);
100+
});
101+
});
102+
103+
it('skip', function (done) {
104+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
105+
assert.strictEqual(client.reply, 'ON');
106+
client.batch()
107+
.set('hello', 'world')
108+
.client('REPLY', 'SKIP', helper.isUndefined())
109+
.set('foo', 'bar', helper.isUndefined())
110+
.get('foo')
111+
.exec(function (err, res) {
112+
assert.strictEqual(client.reply, 'ON');
113+
assert.deepEqual(res, ['OK', undefined, undefined, 'bar']);
114+
done(err);
115+
});
52116
});
53117
});
54118
});

test/helper.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ if (!process.env.REDIS_TESTS_STARTED) {
2929
});
3030
}
3131

32+
function arrayHelper (results) {
33+
if (results instanceof Array) {
34+
assert.strictEqual(results.length, 1, 'The array length may only be one element');
35+
return results[0];
36+
}
37+
return results;
38+
}
39+
3240
module.exports = {
3341
redisProcess: function () {
3442
return rp;
@@ -52,27 +60,38 @@ module.exports = {
5260
},
5361
isNumber: function (expected, done) {
5462
return function (err, results) {
55-
assert.strictEqual(null, err, 'expected ' + expected + ', got error: ' + err);
56-
assert.strictEqual(expected, results, expected + ' !== ' + results);
63+
assert.strictEqual(err, null, 'expected ' + expected + ', got error: ' + err);
64+
results = arrayHelper(results);
65+
assert.strictEqual(results, expected, expected + ' !== ' + results);
5766
assert.strictEqual(typeof results, 'number', 'expected a number, got ' + typeof results);
5867
if (done) done();
5968
};
6069
},
6170
isString: function (str, done) {
6271
str = '' + str; // Make sure it's a string
6372
return function (err, results) {
64-
assert.strictEqual(null, err, "expected string '" + str + "', got error: " + err);
73+
assert.strictEqual(err, null, "expected string '" + str + "', got error: " + err);
74+
results = arrayHelper(results);
6575
if (Buffer.isBuffer(results)) { // If options are passed to return either strings or buffers...
6676
results = results.toString();
6777
}
68-
assert.strictEqual(str, results, str + ' does not match ' + results);
78+
assert.strictEqual(results, str, str + ' does not match ' + results);
6979
if (done) done();
7080
};
7181
},
7282
isNull: function (done) {
7383
return function (err, results) {
74-
assert.strictEqual(null, err, 'expected null, got error: ' + err);
75-
assert.strictEqual(null, results, results + ' is not null');
84+
assert.strictEqual(err, null, 'expected null, got error: ' + err);
85+
results = arrayHelper(results);
86+
assert.strictEqual(results, null, results + ' is not null');
87+
if (done) done();
88+
};
89+
},
90+
isUndefined: function (done) {
91+
return function (err, results) {
92+
assert.strictEqual(err, null, 'expected null, got error: ' + err);
93+
results = arrayHelper(results);
94+
assert.strictEqual(results, undefined, results + ' is not undefined');
7695
if (done) done();
7796
};
7897
},
@@ -91,27 +110,39 @@ module.exports = {
91110
isType: {
92111
number: function (done) {
93112
return function (err, results) {
94-
assert.strictEqual(null, err, 'expected any number, got error: ' + err);
113+
assert.strictEqual(err, null, 'expected any number, got error: ' + err);
95114
assert.strictEqual(typeof results, 'number', results + ' is not a number');
96115
if (done) done();
97116
};
98117
},
118+
string: function (done) {
119+
return function (err, results) {
120+
assert.strictEqual(err, null, 'expected any string, got error: ' + err);
121+
assert.strictEqual(typeof results, 'string', results + ' is not a string');
122+
if (done) done();
123+
};
124+
},
99125
positiveNumber: function (done) {
100126
return function (err, results) {
101-
assert.strictEqual(null, err, 'expected positive number, got error: ' + err);
102-
assert.strictEqual(true, (results > 0), results + ' is not a positive number');
127+
assert.strictEqual(err, null, 'expected positive number, got error: ' + err);
128+
assert(results > 0, results + ' is not a positive number');
103129
if (done) done();
104130
};
105131
}
106132
},
107133
match: function (pattern, done) {
108134
return function (err, results) {
109-
assert.strictEqual(null, err, 'expected ' + pattern.toString() + ', got error: ' + err);
135+
assert.strictEqual(err, null, 'expected ' + pattern.toString() + ', got error: ' + err);
136+
results = arrayHelper(results);
110137
assert(pattern.test(results), "expected string '" + results + "' to match " + pattern.toString());
111138
if (done) done();
112139
};
113140
},
114141
serverVersionAtLeast: function (connection, desired_version) {
142+
// Wait until a connection has established (otherwise a timeout is going to be triggered at some point)
143+
if (Object.keys(connection.server_info).length === 0) {
144+
throw new Error('Version check not possible as the client is not yet ready or did not expose the version');
145+
}
115146
// Return true if the server version >= desired_version
116147
var version = connection.server_info.versions;
117148
for (var i = 0; i < 3; i++) {

0 commit comments

Comments
 (0)