Skip to content

Commit

Permalink
Merge pull request #105 from dguo/key_prefixing
Browse files Browse the repository at this point in the history
Add support for transparent key prefixing. Close #95
  • Loading branch information
luin committed Jul 23, 2015
2 parents 0302390 + 1a42856 commit 3f9014c
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 8 deletions.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A delightful, performance-focused Redis client for Node and io.js

Support Redis >= 2.6.12 and (Node.js >= 0.10.16 or io.js).

# Feature
# Features
ioredis is a robust, full-featured Redis client
used in the world's biggest online commerce company [Alibaba](http://www.alibaba.com/).

Expand All @@ -24,6 +24,7 @@ used in the world's biggest online commerce company [Alibaba](http://www.alibaba
0. Supports offline queue and ready checking.
0. Supports ES6 types such as `Map` and `Set`.
0. Sophisticated error handling strategy.
0. Transparent key prefixing.

<hr>

Expand Down Expand Up @@ -625,6 +626,16 @@ If [hiredis](https://github.com/redis/hiredis-node) is installed(by `npm install
ioredis will use it by default. Otherwise, a pure JavaScript parser will be used.
Typically there's not much differences between them in terms of performance.

## Transparent Key Prefixing
This feature allows you to specify a string that will automatically be prepended
to all the keys in a command, which makes it easier to manage your key
namespaces.

```javascript
var fooRedis = new Redis({ keyPrefix: 'foo:' });
fooRedis.set('bar', 'baz'); // Actually sends SET foo:bar baz
```

<hr>

# Error Handling
Expand Down Expand Up @@ -808,8 +819,6 @@ Ordered by date of first contribution. [Auto-generated](https://github.com/dtrej

# Roadmap

* Transparent Key Prefixing

# Acknowledge

The JavaScript and hiredis parsers are modified from [node_redis](https://github.com/mranney/node_redis) (MIT License, Copyright (c) 2010 Matthew Ranney, http://ranney.com/).
Expand Down
38 changes: 36 additions & 2 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@ var commands = require('../commands');
* @public
*/
function Command(name, args, options, callback) {
if (typeof options === 'undefined') {
options = {};
}
this.name = name;
this.replyEncoding = options && options.replyEncoding;
this.errorStack = options && options.errorStack;
this.replyEncoding = options.replyEncoding;
this.errorStack = options.errorStack;
this.args = args ? _.flatten(args) : [];
var keyPrefix = options.keyPrefix;
if (keyPrefix) {
this._iterateKeys(function (key) {
return keyPrefix + key;
});
}
this.callback = callback;
this.initPromise();
}
Expand Down Expand Up @@ -83,7 +92,24 @@ Command.prototype.getSlot = function () {
};

Command.prototype.getKeys = function () {
return this._iterateKeys();
};

/**
* Iterate through the command arguments that are considered keys.
*
* @param {function} [transform] - The transformation that should be applied to
* each key. The transformations will persist.
* @return {string[]} The keys of the command.
* @private
*/
Command.prototype._iterateKeys = function (transform) {
if (typeof this._keys === 'undefined') {
if (typeof transform !== 'function') {
transform = function (key) {
return key;
};
}
this._keys = [];
var i, keyStart, keyStop;
var def = commands[this.name];
Expand All @@ -93,10 +119,12 @@ Command.prototype.getKeys = function () {
case 'evalsha':
keyStop = parseInt(this.args[1], 10) + 2;
for (i = 2; i < keyStop; ++i) {
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
break;
case 'sort':
this.args[0] = transform(this.args[0]);
this._keys.push(this.args[0]);
for (i = 1; i < this.args.length - 1; ++i) {
if (typeof this.args[i] !== 'string') {
Expand All @@ -106,22 +134,27 @@ Command.prototype.getKeys = function () {
if (directive === 'GET') {
i += 1;
if (this.args[i] !== '#') {
this.args[i] = transform(this.args[i]);
this._keys.push(this.getKeyPart(this.args[i]));
}
} else if (directive === 'BY') {
i += 1;
this.args[i] = transform(this.args[i]);
this._keys.push(this.getKeyPart(this.args[i]));
} else if (directive === 'STORE') {
i += 1;
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
}
break;
case 'zunionstore':
case 'zinterstore':
this.args[0] = transform(this.args[0]);
this._keys.push(this.args[0]);
keyStop = parseInt(this.args[1], 10) + 2;
for (i = 2; i < keyStop; ++i) {
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
break;
Expand All @@ -130,6 +163,7 @@ Command.prototype.getKeys = function () {
keyStop = def.keyStop > 0 ? def.keyStop : this.args.length + def.keyStop + 1;
if (keyStart >= 0 && keyStop <= this.args.length && keyStop > keyStart && def.step > 0) {
for (i = keyStart; i < keyStop; i += def.step) {
this.args[i] = transform(this.args[i]);
this._keys.push(this.args[i]);
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/commander.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ Commander.prototype.send_command = Commander.prototype.call;
* If omit, you have to pass the number of keys as the first argument every time you invoke the command
*/
Commander.prototype.defineCommand = function (name, definition) {
var script = new Script(definition.lua, definition.numberOfKeys);
var script = new Script(definition.lua, definition.numberOfKeys,
this.options.keyPrefix);
this.scriptsSet[name] = script;
this[name] = generateScriptingFunction(script, 'utf8');
this[name + 'Buffer'] = generateScriptingFunction(script, null);
Expand Down Expand Up @@ -109,6 +110,9 @@ function generateFunction (_commandName, _encoding) {
if (this.options.showFriendlyErrorStack) {
options.errorStack = new Error().stack;
}
if (this.options.keyPrefix) {
options.keyPrefix = this.options.keyPrefix;
}

return this.sendCommand(new Command(commandName, args, options, callback));
};
Expand Down
4 changes: 3 additions & 1 deletion lib/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ try {
* When a new `Redis` instance is created, it will connect to Redis server automatically.
* If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to
* the constructor:
* @param {string} [options.keyPrefix=''] - The prefix to prepend to all keys in a command.
* ```javascript
* var redis = new Redis({ lazyConnect: true });
Expand Down Expand Up @@ -165,7 +166,8 @@ Redis.defaultOptions = {
enableReadyCheck: true,
autoResubscribe: true,
autoResendUnfulfilledCommands: true,
lazyConnect: false
lazyConnect: false,
keyPrefix: ''
};

Redis.prototype.resetCommandQueue = function () {
Expand Down
6 changes: 5 additions & 1 deletion lib/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ var Command = require('./command');
var crypto = require('crypto');
var Promise = require('bluebird');

function Script(lua, numberOfKeys) {
function Script(lua, numberOfKeys, keyPrefix) {
this.lua = lua;
this.sha = crypto.createHash('sha1').update(this.lua).digest('hex');
this.numberOfKeys = typeof numberOfKeys === 'number' ? numberOfKeys : null;
this.keyPrefix = keyPrefix ? keyPrefix : '';
}

Script.prototype.execute = function (container, args, options, callback) {
if (typeof this.numberOfKeys === 'number') {
args.unshift(this.numberOfKeys);
}
if (this.keyPrefix) {
options.keyPrefix = this.keyPrefix;
}

var evalsha = new Command('evalsha', [this.sha].concat(args), options);
evalsha.isCustomCommand = true;
Expand Down
15 changes: 15 additions & 0 deletions test/functional/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ describe('pipeline', function () {
expect(pipeline.options).to.have.property('showFriendlyErrorStack', true);
});

it('should support key prefixing', function (done) {
var redis = new Redis({ keyPrefix: 'foo:' });
redis.pipeline().set('bar', 'baz').get('bar').lpush('app1', 'test1').lpop('app1').keys('*').exec(function (err, results) {
expect(err).to.eql(null);
expect(results).to.eql([
[null, 'OK'],
[null, 'baz'],
[null, 1],
[null, 'test1'],
[null, ['foo:bar']]
]);
done();
});
});

describe('#addBatch', function () {
it('should accept commands in constructor', function (done) {
var redis = new Redis();
Expand Down
14 changes: 14 additions & 0 deletions test/functional/scripting.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,18 @@ describe('scripting', function () {
});
});
});

it('should support key prefixing', function (done) {
var redis = new Redis({ keyPrefix: 'foo:' });

redis.defineCommand('echo', {
numberOfKeys: 2,
lua: 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}'
});

redis.echo('k1', 'k2', 'a1', 'a2', function (err, result) {
expect(result).to.eql(['foo:k1', 'foo:k2', 'a1', 'a2']);
done();
});
});
});
71 changes: 71 additions & 0 deletions test/functional/send_command.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,75 @@ describe('send command', function () {
done();
});
});

it('should support key prefixing', function (done) {
var redis = new Redis({ keyPrefix: 'foo:' });
redis.set('bar', 'baz');
redis.get('bar', function (err, result) {
expect(result).to.eql('baz');
redis.keys('*', function (err, result) {
expect(result).to.eql(['foo:bar']);
done();
});
});
});

it('should support key prefixing with multiple keys', function (done) {
var redis = new Redis({ keyPrefix: 'foo:' });
redis.lpush('app1', 'test1');
redis.lpush('app2', 'test2');
redis.lpush('app3', 'test3');
redis.blpop('app1', 'app2', 'app3', 0, function (err, result) {
expect(result).to.eql(['foo:app1', 'test1']);
redis.keys('*', function (err, result) {
expect(result).to.have.members(['foo:app2', 'foo:app3']);
done();
});
});
});

it('should support key prefixing for zunionstore', function (done) {
var redis = new Redis({ keyPrefix: 'foo:' });
redis.zadd('zset1', 1, 'one');
redis.zadd('zset1', 2, 'two');
redis.zadd('zset2', 1, 'one');
redis.zadd('zset2', 2, 'two');
redis.zadd('zset2', 3, 'three');
redis.zunionstore('out', 2, 'zset1', 'zset2', 'WEIGHTS', 2, 3, function (err, result) {
expect(result).to.eql(3);
redis.keys('*', function (err, result) {
expect(result).to.have.members(['foo:zset1', 'foo:zset2', 'foo:out']);
done();
});
});
});

it('should support key prefixing for sort', function (done) {
var redis = new Redis({ keyPrefix: 'foo:' });
redis.hset('object_1', 'name', 'better');
redis.hset('weight_1', 'value', '20');
redis.hset('object_2', 'name', 'best');
redis.hset('weight_2', 'value', '30');
redis.hset('object_3', 'name', 'good');
redis.hset('weight_3', 'value', '10');
redis.lpush('src', '1', '2', '3');
redis.sort('src', 'BY', 'weight_*->value', 'GET', 'object_*->name', 'STORE', 'dest', function (err, result) {
redis.lrange('dest', 0, -1, function (err, result) {
expect(result).to.eql(['good', 'better', 'best']);
redis.keys('*', function (err, result) {
expect(result).to.have.members([
'foo:object_1',
'foo:weight_1',
'foo:object_2',
'foo:weight_2',
'foo:object_3',
'foo:weight_3',
'foo:src',
'foo:dest'
]);
done();
});
});
});
});
});

0 comments on commit 3f9014c

Please sign in to comment.