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

Cache segment validation #183

Merged
merged 1 commit into from
Oct 19, 2012
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ Response validation can only be performed on object responses and will otherwise
* `client` - Sends the Cache-Control HTTP header on the response to support client caching
* `server` - Caches the route on the server only
* `none` - Disable cache for the route on both the client and server
* `segment` - Optional segment name, used to isolate cached items within the cache partition. Defaults to '#name' for server helpers and the path fingerprint (the route path with parameters represented by a '?' character) for routes. Note that when using the MongoDB cache strategy, some paths will require manual override as their name will conflict with MongoDB collection naming rules.
* `expiresIn` - relative expiration expressed in the number of milliseconds since the item was saved in the cache. Cannot be used together with `expiresAt`.
* `expiresAt` - time of day expressed in 24h notation using the 'MM:HH' format, at which point all cache records for the route expire. Cannot be used together with `expiresIn`.

Expand Down
30 changes: 23 additions & 7 deletions lib/cache/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ internals.Client.prototype.stop = function () {
};


internals.Client.prototype.validateSegmentName = function (name) {

if (!this.connection) {
return new Error('Disconnected');
}

return this.connection.validateSegmentName(name);
};


internals.Client.prototype.get = function (key, callback) {

var self = this;
Expand Down Expand Up @@ -143,16 +153,21 @@ internals.Client.prototype.drop = function (key, callback) {
};


exports.Policy = internals.Policy = function (segment, config, cache) {
exports.Policy = internals.Policy = function (config, cache) {

Utils.assert(this.constructor === internals.Policy, 'Cache Policy must be instantiated using new');
Utils.assert(segment, 'Invalid segment configuration');

this._segment = segment;
this._cache = cache;
this.rule = exports.compile(config);

Utils.assert(!this.isMode('server') || this._cache, 'No cache configured for server-side caching');
if (this.isMode('server')) {
Utils.assert(cache, 'No cache configured for server-side caching');

var nameErr = cache.validateSegmentName(config.segment);
Utils.assert(nameErr === null, 'Invalid segment name: ' + config.segment + (nameErr ? ' (' + nameErr.message + ')' : ''));

this._cache = cache;
this._segment = config.segment;
}

return this;
};
Expand Down Expand Up @@ -234,7 +249,8 @@ exports.compile = function (config) {
* expiresIn: 30000,
* expiresAt: '13:00',
* staleIn: 20000,
* staleTimeout: 500
* staleTimeout: 500,
* segment: '/path'
* }
*/

Expand All @@ -257,7 +273,7 @@ exports.compile = function (config) {
});

if (Object.keys(rule.mode).length === 0) {
Utils.assert(Object.keys(config).length === 1, 'Cannot configure cache rules when mode is none');
Utils.assert(!config.expiresIn && !config.expiresAt && !config.staleIn && !config.staleTimeout, 'Cannot configure cache rules when mode is none');
return rule;
}

Expand Down
69 changes: 50 additions & 19 deletions lib/cache/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,27 @@ exports.Connection = internals.Connection = function (options) {

Utils.assert(this.constructor === internals.Connection, 'MongoDB cache client must be instantiated using new');

/*
Database names:

- empty string is not valid
- cannot contain space, "*<>:|?
- should be all lowercase
- limited to 64 bytes (after conversion to UTF-8)
- admin, local and config are reserved
*/

Utils.assert(options.partition !== 'admin' && options.partition !== 'local' && options.partition !== 'config', 'Cache partition name cannot be "admin", "local", or "config" when using MongoDB');
Utils.assert(options.partition.length < 64, 'Cache partition must be less than 64 bytes when using MongoDB');
Utils.assert(options.partition === options.partition.toLowerCase(), 'Cache partition name must be all lowercase when using MongoDB');

this.settings = options;
this.client = null;
this.isReady = false;
this.collections = {};
return this;
};

/*
Database names:

- empty string is not valid
- cannot contain space, ".", "$", "/", "\\" or "\0"
- should be all lowercase
- limited to 64 bytes (after conversion to UTF-8)
- admin, local and config are reserved

Collection names:

- empty string is not valid
- cannot contain "\0"
- avoid creating any collections with "system." prefix
- user created collections should not contain "$" in the name

Also, the sum of the database name + collection name + 1 is limited to
121 bytes (in practice stay below 100).
*/

internals.Connection.prototype.start = function (callback) {

Expand Down Expand Up @@ -79,6 +74,42 @@ internals.Connection.prototype.start = function (callback) {
};


internals.Connection.prototype.validateSegmentName = function (name) {

/*
Collection names:

- empty string is not valid
- cannot contain "\0"
- avoid creating any collections with "system." prefix
- user created collections should not contain "$" in the name
- database name + collection name < 100 (actual 120)
*/

if (!name) {
return new Error('Empty string');
}

if (name.indexOf('\0') !== -1) {
return new Error('Includes null character');
}

if (name.indexOf('system.') !== 0) {
return new Error('Begins with "system."');
}

if (name.indexOf('$') !== -1) {
return new Error('Contains "$"');
}

if (name.length + this.settings.partition.length >= 100) {
return new Error('Segment and partition name lengths exceeds 100 characters');
}

return null;
};


internals.Connection.prototype.getCollection = function (name, callback) {

var self = this;
Expand Down
14 changes: 14 additions & 0 deletions lib/cache/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ internals.Connection.prototype.stop = function () {
};


internals.Connection.prototype.validateSegmentName = function (name) {

if (!name) {
return new Error('Empty string');
}

if (name.indexOf('\0') !== -1) {
return new Error('Includes null character');
}

return null;
};


internals.Connection.prototype.get = function (key, callback) {

if (!this.client) {
Expand Down
5 changes: 4 additions & 1 deletion lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ exports = module.exports = internals.Route = function (options, server) {
// Cache

Utils.assert(!this.config.cache || this.config.cache.mode === 'none' || this.method === 'get', 'Only GET routes can use a cache');
this.cache = new Cache.Policy(this.fingerprint, this.config.cache, this.server.cache);
if (this.config.cache) {
this.config.cache.segment = this.config.cache.segment || this.fingerprint;
}
this.cache = new Cache.Policy(this.config.cache, this.server.cache);

// Prerequisites

Expand Down
6 changes: 5 additions & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,11 @@ internals.Server.prototype.addHelper = function (name, method, options) {

// Create helper

var cache = new Cache.Policy('#' + name, settings.cache, this.cache);
if (settings.cache) {
settings.cache.segment = settings.cache.segment || '#' + name;
}

var cache = new Cache.Policy(settings.cache, this.cache);

var log = function (tags, data) {

Expand Down
27 changes: 22 additions & 5 deletions test/unit/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ require('../suite').Server(function(Server, useMongo) {

it('is enabled for both client and server by defaults', function (done) {
var config = {
expiresIn: 50000
expiresIn: 50000,
segment: 'test'
};
var cache = new Cache.Policy('test', config, {});
var client = new Cache.Client(Defaults.cache('redis'));
var cache = new Cache.Policy(config, client);

expect(cache.isMode('server')).to.equal(true);
expect(cache.isMode('client')).to.equal(true);
Expand All @@ -110,7 +112,8 @@ require('../suite').Server(function(Server, useMongo) {
var config = {
mode: 'none'
};
var cache = new Cache.Policy('test', config, {});
var client = new Cache.Client(Defaults.cache('redis'));
var cache = new Cache.Policy(config, client);

expect(cache.isEnabled()).to.equal(false);
expect(Object.keys(cache.rule.mode).length).to.equal(0);
Expand All @@ -124,7 +127,21 @@ require('../suite').Server(function(Server, useMongo) {
expiresIn: 50000
};
var fn = function () {
var cache = new Cache.Policy('test', config, {});
var cache = new Cache.Policy(config, {});
};

expect(fn).to.throw(Error);

done();
});

it('throws an error when segment is missing', function (done) {
var config = {
expiresIn: 50000
};
var fn = function () {
var client = new Cache.Client(Defaults.cache('redis'));
var cache = new Cache.Policy(config, client);
};

expect(fn).to.throw(Error);
Expand Down Expand Up @@ -278,7 +295,7 @@ require('../suite').Server(function(Server, useMongo) {
staleTimeout: 500
};
var fn = function () {
var cache = new Cache.Policy('test', config, {});
var cache = new Cache.Policy(config, {});
};

expect(fn).to.throw(Error);
Expand Down