From 45ebc59a132630110d77b81fe4797006bf1f4109 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 17 Aug 2015 14:44:48 +0200 Subject: [PATCH] Exposes LockingCache configuration to adjust ttl and expire policy LockingCache now accepts deleteOnHit option to delete objects when they are requested even if they were set with a ttl/timeout > 0. MapnikSource now accepts configuring ttl and timeout/ttl for the internal locking cache so it's possible to combine it with metatiling in a more efficient manner. Now it's possible to use metatile, use a ttl>0 and remove on hit, so cached objects from metatile will be removed after ttl but objects already requested are removed as they get a hit. --- lib/lockingcache.js | 15 ++++-- lib/mapnik_backend.js | 16 ++++-- test/lockingcache.test.js | 47 +++++++++++++++++ test/uri.test.js | 103 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 test/uri.test.js diff --git a/lib/lockingcache.js b/lib/lockingcache.js index d2debfb..18e51b0 100644 --- a/lib/lockingcache.js +++ b/lib/lockingcache.js @@ -1,5 +1,5 @@ module.exports = LockingCache; -function LockingCache(generate, timeout) { +function LockingCache(generate, options) { this.callbacks = {}; this.timeouts = {}; this.results = {}; @@ -7,10 +7,19 @@ function LockingCache(generate, timeout) { // 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) { @@ -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); diff --git a/lib/mapnik_backend.js b/lib/mapnik_backend.js index 6b4162e..13905ae 100644 --- a/lib/mapnik_backend.js +++ b/lib/mapnik_backend.js @@ -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; }; @@ -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); @@ -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; @@ -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. diff --git a/test/lockingcache.test.js b/test/lockingcache.test.js index 7476c6c..057d97f 100644 --- a/test/lockingcache.test.js +++ b/test/lockingcache.test.js @@ -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'); + }); + }); }); diff --git a/test/uri.test.js b/test/uri.test.js new file mode 100644 index 0000000..1568ed1 --- /dev/null +++ b/test/uri.test.js @@ -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); + }); + }); + }); +});