Skip to content

Commit

Permalink
merge get with getOrGenerate. Closes #74. Fixes #76 Fixes #77 Fixes #78.
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed Sep 6, 2014
1 parent ba940de commit c002f27
Show file tree
Hide file tree
Showing 5 changed files with 710 additions and 170 deletions.
45 changes: 27 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,14 @@ The object is constructed using `new Policy(options, [cache, segment])` where:
expire. Uses local time. Cannot be used together with `expiresIn`.
- `staleIn` - number of milliseconds to mark an item stored in cache as stale and reload it. Must be less than `expiresIn`.
- `staleTimeout` - number of milliseconds to wait before checking if an item is stale.
- `generateTimeout` - number of milliseconds to wait before returning a timeout error when the `getOrGenerate()` `generateFunc` function
- `generateFunc` - a function used to generate a new cache item if one is not found in the cache when calling `get()`. The method's
signature is `function(id, next)` where:
- `id` - the `id` string or object provided to the `get()` method.
- `next` - the method called when the new item is returned with the signature `function(err, value, ttl)` where:
- `err` - an error condition.
- `value` - the new value generated.
- `ttl` - the cache ttl value in milliseconds. Set to `0` to skip storing in the cache. Defaults to the cache global policy.
- `generateTimeout` - number of milliseconds to wait before returning a timeout error when the `generateFunc` function
takes too long to return a value. When the value is eventually returned, it is stored in the cache for future requests.
- `cache` - a `Client` instance (which has already been started).
- `segment` - required when `cache` is provided. The segment name used to isolate cached items within the cache partition.
Expand All @@ -90,10 +97,24 @@ The object is constructed using `new Policy(options, [cache, segment])` where:

The `Policy` object provides the following methods:

- `get(id, callback)` - retrieve an item from the cache where:
- `id` - the unique item identifier (within the policy segment).
- `callback` - a function with the signature `function(err, cached)` where `cached` is the object returned by the `client.get()` with
the additional `isStale` boolean key.
- `get(id, callback)` - retrieve an item from the cache. If the item is not found and the `generateFunc` method was provided, a new value
is generated, stored in the cache, and returned. the method arguments are:
- `id` - the unique item identifier (within the policy segment). Can be a string or an object with the required 'id' key.
- `callback` - the return function. The function signature is based on the `generateFunc` settings. If the `generateFunc` is not set,
the signature is `function(err, cached)`. Otherwise, the signature is `function(err, value, cached, report)` where:
- `err` - any errors encountered.
- `value` - the fetched or generated value.
- `cached` - `null` if a valid item was not found in the cache, or an object with the following keys:
- `item` - the cached `value`.
- `stored` - the timestamp when the item was stored in the cache.
- `ttl` - the cache ttl value for the record.
- `isStale` - `true` if the item is stale.
- `report` - an object with logging information about the generation operation containing the following keys (as relevant):
- `msec` - the cache lookup time in milliseconds.
- `stored` - the timestamp when the item was stored in the cache.
- `isStale` - `true` if the item is stale.
- `ttl` - the cache ttl value for the record.
- `error` - lookup error.
- `set(id, value, ttl, callback)` - store an item in the cache where:
- `id` - the unique item identifier (within the policy segment).
- `value` - the string or object value to be stored.
Expand All @@ -104,16 +125,4 @@ The `Policy` object provides the following methods:
- `id` - the unique item identifier (within the policy segment).
- `callback` - a function with the signature `function(err)`.
- `ttl(created)` - given a `created` timestamp in milliseconds, returns the time-to-live left based on the configured rules.
- `getOrGenerate(id, generateFunc, callback)` - get an item from the cache if found, otherwise calls the `generateFunc` to produce a new value
and stores it in the cache. This method applies the staleness rules. Its arguments are:
- `id` - the unique item identifier (within the policy segment).
- `generateFunc` - the function used to generate a new cache item if one is not found in the cache. The method's signature is
`function(err, value, ttl)` where:
- `err` - an error condition.
- `value` - the new value generated.
- `ttl` - the cache ttl value in milliseconds. Set to `0` to skip storing in the cache. Defaults to the cache global policy.
- `callback` - a function with the signature `function(err, value, cached, report)` where:
- `err` - any errors encountered.
- `value` - the fetched or generated value.
- `cached` - the `cached` object returned by `policy.get()` is the item was found in the cache.
- `report` - an object with logging information about the operation.

20 changes: 8 additions & 12 deletions examples/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,7 @@ internals.handler = function (req, res) {

internals.getResponse = function (callback) {

var logFunc = function (item) {

console.log(item);
};

var generateFunc = function (next) {

next(null, 'my example');
};

internals.policy.getOrGenerate('myExample', logFunc, generateFunc, callback);
internals.policy.get('myExample', callback);
};


Expand All @@ -45,7 +35,13 @@ internals.startCache = function (callback) {
};

var policyOptions = {
expiresIn: 5000
expiresIn: 5000,
generateFunc: function (id, next) {

var item = 'example';
console.log(item);
return next(null, item);
}
};

var client = new Catbox.Client(require('../test/import'), clientOptions);
Expand Down
181 changes: 99 additions & 82 deletions lib/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,103 +27,79 @@ exports = module.exports = internals.Policy = function (options, cache, segment)
};


internals.Policy.prototype.get = function (id, callback) {
internals.Policy.prototype.get = function (key, callback) { // key: string or { id: 'id' }

var self = this;

if (!this._cache) {
return callback(null, null);
}
var id = typeof key === 'object' ? key.id : key;

this._cache.get({ segment: this._segment, id: id }, function (err, cached) {

if (err) {
return callback(err);
}
if (!this.rule.generateFunc) {
return this._get(id, callback);
}

if (cached) {
cached.isStale = (self.rule.staleIn ? (Date.now() - cached.stored) >= self.rule.staleIn : false);
}
// Lookup in cache

return callback(null, cached);
});
};
var timer = new Hoek.Timer();
this._get(id, function (err, cached) {

// Error / Not found

internals.Policy.prototype.set = function (id, value, ttl, callback) {
if (err || !cached) {
return self._validate(id, key, null, { msec: timer.elapsed(), error: err }, callback);
}

callback = callback || Hoek.ignore;
// Found

if (!this._cache) {
return callback(null);
}
var report = {
msec: timer.elapsed(),
stored: cached.stored,
ttl: cached.ttl,
isStale: cached.isStale
};

ttl = ttl || internals.Policy.ttl(this.rule);
this._cache.set({ segment: this._segment, id: id }, value, ttl, callback);
return self._validate(id, key, cached, report, callback);
});
};


internals.Policy.prototype.drop = function (id, callback) {
internals.Policy.prototype.getOrGenerate = function (id, generateFunc, callback) { // For backwards compatibility

callback = callback || Hoek.ignore;

if (!this._cache) {
return callback(null);
}

this._cache.drop({ segment: this._segment, id: id }, callback);
};
var self = this;

this.rule.generateFunc = function (id, next) {

internals.Policy.prototype.ttl = function (created) {
self.rule.generateFunc = null;
return generateFunc(next);
};

return internals.Policy.ttl(this.rule, created);
return this.get(id, callback);
};


internals.Policy.prototype.getOrGenerate = function (id, generateFunc, callback) {

var self = this;

// Check if cache enabled
internals.Policy.prototype._get = function (id, callback) {

if (!this._cache) {
return self._validate(id, null, generateFunc, null, callback);
return callback(null, null);
}

// Lookup in cache

var timer = new Hoek.Timer();
this.get(id, function (err, cached) {
var rule = this.rule;

// Error
this._cache.get({ segment: this._segment, id: id }, function (err, cached) {

if (err) {
report = err;
return self._validate(id, null, generateFunc, err, callback);
return callback(err);
}

// Not found

if (!cached) {
return self._validate(id, null, generateFunc, { msec: timer.elapsed() }, callback);
if (cached) {
cached.isStale = (rule.staleIn ? (Date.now() - cached.stored) >= rule.staleIn : false);
}

// Found

var report = {
msec: timer.elapsed(),
stored: cached.stored,
ttl: cached.ttl,
isStale: cached.isStale
};

return self._validate(id, cached, generateFunc, report, callback);
return callback(null, cached);
});
};


internals.Policy.prototype._validate = function (id, cached, generateFunc, report, callback) {
internals.Policy.prototype._validate = function (id, key, cached, report, callback) {

var self = this;

Expand Down Expand Up @@ -164,7 +140,7 @@ internals.Policy.prototype._validate = function (id, cached, generateFunc, repor

// Generate new value

generateFunc.call(null, function (err, value, ttl) {
this.rule.generateFunc.call(null, key, function (err, value, ttl) {

// Error or not cached

Expand All @@ -182,15 +158,48 @@ internals.Policy.prototype._validate = function (id, cached, generateFunc, repor
};


internals.Policy.prototype.set = function (key, value, ttl, callback) {

callback = callback || Hoek.ignore;

if (!this._cache) {
return callback(null);
}

ttl = ttl || internals.Policy.ttl(this.rule);
var id = (key && typeof key === 'object') ? key.id : key;
this._cache.set({ segment: this._segment, id: id }, value, ttl, callback);
};


internals.Policy.prototype.drop = function (id, callback) {

callback = callback || Hoek.ignore;

if (!this._cache) {
return callback(null);
}

this._cache.drop({ segment: this._segment, id: id }, callback);
};


internals.Policy.prototype.ttl = function (created) {

return internals.Policy.ttl(this.rule, created);
};


internals.Policy.compile = function (options, serverSide) {
/*
* {
* expiresIn: 30000,
* expiresAt: '13:00',
* staleIn: 20000,
* staleTimeout: 500,
* generateTimeout: 500
* }
{
expiresIn: 30000,
expiresAt: '13:00',
staleIn: 20000,
staleTimeout: 500,
generateFunc: function (id, next) { next(err, result, ttl); }
generateTimeout: 500
}
*/

var rule = {};
Expand All @@ -209,6 +218,8 @@ internals.Policy.compile = function (options, serverSide) {
Hoek.assert(!(!!options.staleIn ^ !!options.staleTimeout), 'Rule must include both of staleIn and staleTimeout or none'); // XNOR
Hoek.assert(!options.staleTimeout || !options.expiresIn || options.staleTimeout < options.expiresIn, 'staleTimeout must be less than expiresIn');
Hoek.assert(!options.staleTimeout || !options.expiresIn || options.staleTimeout < (options.expiresIn - options.staleIn), 'staleTimeout must be less than the delta between expiresIn and staleIn');
// Hoek.assert(options.generateFunc || !options.generateTimeout, 'Rule cannot include generateTimeout without generateFunc'); // Disabled for backwards compatibility
Hoek.assert(!options.generateFunc || typeof options.generateFunc === 'function', 'generateFunc must be a function');

// Expiration

Expand Down Expand Up @@ -241,17 +252,21 @@ internals.Policy.compile = function (options, serverSide) {

// generateTimeout

if (options.generateTimeout) {
if (options.generateFunc) {
rule.generateFunc = options.generateFunc;
}

if (options.generateTimeout) { // Keep outside options.generateFunc condition for backwards compatibility
rule.generateTimeout = options.generateTimeout;
}

return rule;
};


internals.Policy.ttl = function (rule, created) {
internals.Policy.ttl = function (rule, created, now) {

var now = Date.now();
now = now || Date.now();
created = created || now;
var age = now - created;

Expand All @@ -260,28 +275,30 @@ internals.Policy.ttl = function (rule, created) {
}

if (rule.expiresIn) {
var ttl = rule.expiresIn - age;
return (ttl > 0 ? ttl : 0); // Can be negative
return Math.max(rule.expiresIn - age, 0);
}

if (rule.expiresAt) {
if (created !== now &&
now - created > internals.day) { // If the item was created more than a 24 hours ago

if (age > internals.day) { // If the item was created more than a 24 hours ago
return 0;
}

var expiresAt = new Date(created); // Assume everything expires in relation to now
var expiresAt = new Date(created); // Compare expiration time on the same day
expiresAt.setHours(rule.expiresAt.hours);
expiresAt.setMinutes(rule.expiresAt.minutes);
expiresAt.setSeconds(0);
expiresAt.setMilliseconds(0);
var expires = expiresAt.getTime();

var expiresIn = expiresAt.getTime() - created;
if (expiresIn <= 0) {
expiresIn += internals.day; // Time passed for today, move to tomorrow
if (expires <= created) {
expires += internals.day; // Move to tomorrow
}

if (now >= expires) { // Expired
return 0;
}

return expiresIn - age;
return expires - now;
}

return 0; // No rule
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "catbox",
"description": "Multi-strategy object caching service",
"version": "3.1.0",
"version": "3.2.0",
"author": "Eran Hammer <eran@hammer.io> (http://hueniverse.com)",
"contributors": [
"Wyatt Preul <wpreul@gmail.com> (http://jsgeek.com)",
Expand Down
Loading

2 comments on commit c002f27

@hueniverse
Copy link
Contributor

Choose a reason for hiding this comment

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

@nvcexploder Can you review?

@nvcexploder
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good.

Please sign in to comment.