Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for transparent key prefixing. Close #95 #105

Merged
merged 4 commits into from
Jul 23, 2015
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
37 changes: 35 additions & 2 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,18 @@ 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) : [];
if (options.keyPrefix) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be:

var keyPrefix = options.keyPrefix;
if (keyPrefix) {
  this._iterateKeys(function (key) {
    return keyPrefix + key;
  });
}

Otherwise each iteration it will look for a pointer from options to .keyPrefix and we want to avoid it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Thanks.

this._iterateKeys(function (key) {
return options.keyPrefix + key;
});
}
this.callback = callback;
this.initPromise();
}
Expand Down Expand Up @@ -83,7 +91,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 +118,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 +133,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 +162,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();
});
});
});
});
});