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

Exposes LockingCache configuration to adjust ttl and expire policy #97

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
15 changes: 12 additions & 3 deletions lib/lockingcache.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
module.exports = LockingCache;
function LockingCache(generate, timeout) {
function LockingCache(generate, options) {
this.callbacks = {};
this.timeouts = {};
this.results = {};

// When there's no generator function, you
this.generate = generate || function() {};

options = isFinite(options) ? { timeout: options } : (options || {});

// Timeout cached objects after 1 minute by default.
// A value of 0 will cause it to not cache at all beyond, just return the result to
// all locked requests.
this.timeout = (typeof timeout === "undefined") ? 60000 : timeout;
this.timeout = (typeof options.timeout === "undefined") ? 60000 : options.timeout;

// Cached results will be always removed when deleteOnHit is set to true
// That allows to set a timeout > 0 but removing objects from the cache when they
// are returned from the cache.
// This option is useful when using metatile > 1 because you can evict results
// put here because of metatiling with a timeout and remove the ones that get a hit
this.deleteOnHit = options.deleteOnHit || false;
}

LockingCache.prototype.get = function(id, callback) {
Expand Down Expand Up @@ -60,7 +69,7 @@ LockingCache.prototype.trigger = function(id) {
var data = this.results[id];
var callbacks = this.callbacks[id] || [];

if (this.timeout === 0) {
if (this.timeout === 0 || this.deleteOnHit) {
// instant purge with the first put() for a key
// clears timeouts and results
this.del(id);
Expand Down
16 changes: 13 additions & 3 deletions lib/mapnik_backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ MapnikSource.prototype._normalizeURI = function(uri) {
// autoload fonts unless explicitly set to false
if (typeof uri.query.autoLoadFonts === "undefined") uri.query.autoLoadFonts = true;
else uri.query.autoLoadFonts = as_bool(uri.query.autoLoadFonts);
uri.query.metatileCache = uri.query.metatileCache || {};
// Time to live in ms for cached tiles/grids
// When set to 0 and `deleteOnHit` set to `false` object won't be removed
// from cache until they are requested
// When set to > 0 objects will be removed from cache after the number of ms
uri.query.metatileCache.ttl = uri.query.metatileCache.ttl || 0;
// Overrides object removal behaviour when ttl>0 by removing objects from
// from cache even if they had a ttl set
uri.query.metatileCache.deleteOnHit = uri.query.metatileCache.hasOwnProperty('deleteOnHit') ?
as_bool(uri.query.metatileCache.deleteOnHit) : false;
return uri;
};

Expand Down Expand Up @@ -160,7 +170,7 @@ MapnikSource.prototype._open = function(uri) {
// https://github.com/mapbox/tilelive-mapnik/issues/25
// there seems to be no value to assinging xml to a property
//source._xml = xml;
source._createMetatileCache();
source._createMetatileCache(uri.query.metatileCache);
source._createPool(xml, this);
}, function(err) {
if (err) return error(err);
Expand Down Expand Up @@ -277,7 +287,7 @@ MapnikSource.prototype._populateInfo = function(callback) {

// Creates a locking cache that generates tiles. When requesting the same tile
// multiple times, they'll be grouped to one request.
MapnikSource.prototype._createMetatileCache = function() {
MapnikSource.prototype._createMetatileCache = function(options) {
var source = this;
this._tileCache = new LockingCache(function(key) {
var cache = this;
Expand Down Expand Up @@ -309,7 +319,7 @@ MapnikSource.prototype._createMetatileCache = function() {
// as part of this metatile.
return keys;
},
0); // purge immediately after callbacks
{ timeout: options.ttl, deleteOnHit: options.deleteOnHit }); // purge immediately after callbacks
};

// Render handler for a given tile request.
Expand Down
47 changes: 47 additions & 0 deletions test/lockingcache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,51 @@ describe('locking cache', function() {
assert.equal(value, '=0');
});
});

it('test deleteOnHit with timeout=20', function(done) {
var n = 0;
var cache = new LockingCache(function generate(key) {
var extraKey = key + '_extra';
process.nextTick(function() {
cache.put(extraKey, null, '=' + n);
cache.put(key, null, '=' + n++);
});
return [key, extraKey];
}, {timeout: 20, deleteOnHit: true});

cache.get('key', function(err, value) {
// (A) should get the first result
assert.ok(!err);
assert.equal(value, '=0');

cache.get('key', function(err, value) {
// (D) should get the second result. The first result should be
// timed-out already even though we're calling it in the same tick
// as our cached callbacks (ie. even before B below)
assert.ok(!err);
assert.equal(value, '=1');

setTimeout(function() {
// (E) should get a third result. This first result was consumed
// already consumed in (C) and the second one was expired due timeout
// so a new result will be generated
cache.get('key_extra', function(err, value) {
assert.ok(!err);
assert.equal(value, '=2');
done();
});
}, 25);
});
});
cache.get('key', function(err, value) {
// (B) should get the first result, because it's queued
assert.ok(!err);
assert.equal(value, '=0');
});
cache.get('key_extra', function(err, value) {
// (C) should get the first result
assert.ok(!err);
assert.equal(value, '=0');
});
});
});
103 changes: 103 additions & 0 deletions test/uri.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
var assert = require('assert');
var mapnik_backend = require('..');

describe('uri query options', function() {

describe('metatileCache config', function() {

function makeUri(metatileCache) {
return {
query: {
metatileCache: metatileCache
}
};
}

var backend;

before(function(done) {
backend = new mapnik_backend('mapnik://./test/data/test.xml', done);
});

var scenarios = [
{
desc: 'handles no config as default values',
metatileCache: undefined,
expected: {
ttl: 0,
deleteOnHit: false
}
},
{
desc: 'handles default values',
metatileCache: {},
expected: {
ttl: 0,
deleteOnHit: false
}
},
{
desc: 'handles ttl',
metatileCache: {
ttl: 1000
},
expected: {
ttl: 1000,
deleteOnHit: false
}
},
{
desc: 'handles deleteOnHit',
metatileCache: {
deleteOnHit: false
},
expected: {
ttl: 0,
deleteOnHit: false
}
},
{
desc: 'handles deleteOnHit=true',
metatileCache: {
deleteOnHit: true
},
expected: {
ttl: 0,
deleteOnHit: true
}
},
{
desc: 'handles deleteOnHit="true"',
metatileCache: {
deleteOnHit: 'true'
},
expected: {
ttl: 0,
deleteOnHit: true
}
},
{
desc: 'handles deleteOnHit and ttl',
metatileCache: {
ttl: 1000,
deleteOnHit: true
},
expected: {
ttl: 1000,
deleteOnHit: true
}
}
];

scenarios.forEach(function(scenario) {

it(scenario.desc, function() {
var uri = backend._normalizeURI(makeUri(scenario.metatileCache));

assert.ok(uri.query.metatileCache);
assert.equal(uri.query.metatileCache.ttl, scenario.expected.ttl);
assert.equal(uri.query.metatileCache.deleteOnHit, scenario.expected.deleteOnHit);
});
});
});
});