From 64b4efef17b053a7475ebb0dbdd07415346ce829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Thu, 12 Sep 2019 17:23:19 +0200 Subject: [PATCH 01/18] Do not cache map template CRUD errors in Named Map provider --- .../mapconfig/provider/named-map-provider.js | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/lib/cartodb/models/mapconfig/provider/named-map-provider.js b/lib/cartodb/models/mapconfig/provider/named-map-provider.js index 298920a1..292e5dcb 100644 --- a/lib/cartodb/models/mapconfig/provider/named-map-provider.js +++ b/lib/cartodb/models/mapconfig/provider/named-map-provider.js @@ -48,7 +48,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { this.affectedTablesCache = affectedTablesCache; // providing - this.err = null; this.mapConfig = null; this.rendererParams = null; this.context = {}; @@ -56,13 +55,12 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { } getMapConfig (callback) { - if (!!this.err || this.mapConfig !== null) { - return callback(this.err, this.mapConfig, this.rendererParams, this.context); + if (this.mapConfig !== null) { + return callback(null, this.mapConfig, this.rendererParams, this.context); } this.getContext((err, context) => { if (err) { - this.err = err; return callback(err); } @@ -75,8 +73,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { this.config; } catch (e) { const err = new Error('malformed config parameter, should be a valid JSON'); - this.err = err; - return callback(err); } } @@ -85,7 +81,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { this.getTemplate((err, template) => { if (err) { - this.err = err; return callback(err); } @@ -94,7 +89,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { try { requestMapConfig = this.templateMaps.instance(template, templateParams); } catch (err) { - this.err = err; return callback(err); } @@ -103,7 +97,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { this.mapConfigAdapter.getMapConfig( user, requestMapConfig, rendererParams, context, (err, mapConfig, stats = {}) => { if (err) { - this.err = err; return callback(err); } @@ -148,7 +141,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { this.userLimitsBackend.getRenderLimits(this.user, this.params.api_key, (err, renderLimits) => { if (err) { - this.err = err; return callback(err); } @@ -163,13 +155,12 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { } getTemplate (callback) { - if (!!this.err || this.template !== null) { - return callback(this.err, this.template); + if (this.template !== null) { + return callback(null, this.template); } this.templateMaps.getTemplate(this.user, this.templateName, (err, tpl) => { if (err) { - this.err = err; return callback(err); } @@ -177,8 +168,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { const error = new Error(`Template '${this.templateName}' of user '${this.user}' not found`); error.http_status = 404; - this.err = error; - return callback(error); } @@ -190,15 +179,12 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { const error = new Error('Failed to authorize template'); error.http_status = 403; - this.err = error; - return callback(error); } if (!authorized) { const error = new Error('Unauthorized template instantiation'); error.http_status = 403; - this.err = error; return callback(error); } @@ -222,7 +208,6 @@ module.exports = class NamedMapMapConfigProvider extends BaseMapConfigProvider { this.affectedTables = null; - this.err = null; this.mapConfig = null; this.cacheBuster = Date.now(); From 9d6726227aa5e1fe26be37c8f22ab83e41bb3aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Thu, 12 Sep 2019 18:02:13 +0200 Subject: [PATCH 02/18] Add maxAge param to lru-cache to be able to refresh entries when staled --- lib/cartodb/cache/named_map_provider_cache.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index d0e34883..bb06ac79 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -8,6 +8,8 @@ var queue = require('queue-async'); var LruCache = require("lru-cache"); +const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10; + function NamedMapProviderCache( templateMaps, pgConnection, @@ -23,7 +25,7 @@ function NamedMapProviderCache( this.mapConfigAdapter = mapConfigAdapter; this.affectedTablesCache = affectedTablesCache; - this.providerCache = new LruCache({ max: 2000 }); + this.providerCache = new LruCache({ max: 2000, maxAge: TEN_MINUTES_IN_MILLISECONDS }); } module.exports = NamedMapProviderCache; From 6229455d25da90bc010fce334ca517fdf33ee312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Thu, 12 Sep 2019 20:32:52 +0200 Subject: [PATCH 03/18] Remove mechanism to reset named map's provider as, in the end, it's reading from storage (redis) always so cache isn't doing its job. There is already a mechanism to invalidate cache entry when a template is modified (see template-maps emits on "update" and "delete", and listeners attached at server startup) --- lib/cartodb/cache/named_map_provider_cache.js | 19 +-- test/acceptance/named_maps_cache.js | 145 ++++++++++++------ 2 files changed, 96 insertions(+), 68 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index bb06ac79..e5f63d63 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -4,7 +4,6 @@ var _ = require('underscore'); var dot = require('dot'); var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); var templateName = require('../backends/template_maps').templateName; -var queue = require('queue-async'); var LruCache = require("lru-cache"); @@ -57,23 +56,7 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok var namedMapProvider = namedMapProviders[providerKey]; - var self = this; - queue(2) - .defer(namedMapProvider.getTemplate.bind(namedMapProvider)) - .defer(this.templateMaps.getTemplate.bind(this.templateMaps), user, templateId) - .awaitAll(function templatesQueueDone(err, results) { - if (err) { - return callback(err); - } - - // We want to reset provider its template has changed - // Ideally this should be done in a passive mode where this cache gets notified of template changes - var uniqueFingerprints = _.uniq(results.map(self.templateMaps.fingerPrint)).length; - if (uniqueFingerprints > 1) { - namedMapProvider.reset(); - } - return callback(null, namedMapProvider); - }); + return callback(null, namedMapProvider); }; NamedMapProviderCache.prototype.invalidate = function(user, templateId) { diff --git a/test/acceptance/named_maps_cache.js b/test/acceptance/named_maps_cache.js index 44cfc84e..0cdc2a49 100644 --- a/test/acceptance/named_maps_cache.js +++ b/test/acceptance/named_maps_cache.js @@ -1,13 +1,12 @@ 'use strict'; require('../support/test_helper'); -var RedisPool = require('redis-mpool'); +const helper = require('../support/test_helper'); var assert = require('../support/assert'); var mapnik = require('windshaft').mapnik; var CartodbWindshaft = require('../../lib/cartodb/server'); var serverOptions = require('../../lib/cartodb/server_options'); -var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js'); describe('named maps provider cache', function() { var server; @@ -16,14 +15,8 @@ describe('named maps provider cache', function() { server = new CartodbWindshaft(serverOptions); }); - // configure redis pool instance to use in tests - var redisPool = new RedisPool(global.environment.redis); - - var templateMaps = new TemplateMaps(redisPool, { - max_user_templates: global.environment.maxUserTemplates - }); - var username = 'localhost'; + const apikey = 1234; var templateName = 'template_with_color'; var IMAGE_TOLERANCE = 20; @@ -31,7 +24,7 @@ describe('named maps provider cache', function() { function createTemplate(color) { return { version: '0.0.1', - name: templateName, + name: `${templateName}_${color}`, auth: { method: 'open' }, @@ -56,17 +49,13 @@ describe('named maps provider cache', function() { }; } - afterEach(function (done) { - templateMaps.delTemplate(username, templateName, done); - }); - - function getNamedTile(options, callback) { + function getNamedTile(templateId, options, callback) { if (!callback) { callback = options; options = {}; } - var url = '/api/v1/map/named/' + templateName + '/all/' + [0,0,0].join('/') + '.png'; + var url = '/api/v1/map/named/' + templateId + '/all/' + [0,0,0].join('/') + '.png'; var requestOptions = { url: url, @@ -88,60 +77,116 @@ describe('named maps provider cache', function() { assert.response(server, requestOptions, expectedResponse, function (res, err) { var img; - if (statusCode === 200) { - img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary')); + if (res.statusCode === 200) { + img = mapnik.Image.fromBytes(new Buffer.from(res.body, 'binary')); } return callback(err, res, img); }); } + function addTemplate (template, callback) { + const createTemplateRequest = { + url: `/api/v1/map/named?api_key=${apikey}`, + method: 'POST', + headers: { + host: username, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(template) + }; + + const expectedResponse = { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } + }; + + assert.response(server, createTemplateRequest, expectedResponse, (res, err) => { + let template; + + if (res.statusCode === 200) { + template = JSON.parse(res.body); + } + + return callback(err, res, template); + }); + } + + + function deleteTemplate (templateId, callback) { + const deleteTemplateRequest = { + url: `/api/v1/map/named/${templateId}?api_key=${apikey}`, + method: 'DELETE', + headers: { + host: 'localhost', + } + }; + + const expectedResponse = { + status: 204, + }; + + assert.response(server, deleteTemplateRequest, expectedResponse, (res, err) => { + return callback(err, res); + }); + } + function previewFixture(color) { return './test/fixtures/provider/populated_places_simple_reduced-' + color + '.png'; } - var colors = ['red', 'red', 'green', 'blue']; + var colors = ['black', 'red', 'green', 'blue']; colors.forEach(function(color) { it('should return an image estimating its bounds based on dataset', function (done) { - templateMaps.addTemplate(username, createTemplate(color), function (err) { + addTemplate(createTemplate(color), function (err, res, template) { if (err) { return done(err); } - getNamedTile(function(err, res, img) { + + getNamedTile(template.template_id, function(err, res, img) { assert.ok(!err); - assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, done); - }); - }); - }); - }); + assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, (err) => { + assert.ifError(err); - it('should fail to use template from named map provider after template deletion', function (done) { - var color = 'black'; - templateMaps.addTemplate(username, createTemplate(color), function (err) { - if (err) { - return done(err); - } - getNamedTile(function(err, res, img) { - assert.ok(!err); - assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, function(err) { - assert.ok(!err); - - templateMaps.delTemplate(username, templateName, function (err) { - assert.ok(!err); - - getNamedTile({ statusCode: 404 }, function(err, res) { - assert.ok(!err); - assert.deepEqual( - JSON.parse(res.body).errors, - ["Template 'template_with_color' of user 'localhost' not found"] - ); - - // add template again so it's clean in afterEach - templateMaps.addTemplate(username, createTemplate(color), done); - }); + const keysToDelete = {}; + keysToDelete['map_tpl|localhost'] = 0; + helper.deleteRedisKeys(keysToDelete, done); }); }); }); }); }); + it('should fail to use template from named map provider after template deletion', function (done) { + const color = 'black'; + const templateId = `${templateName}_${color}`; + + addTemplate(createTemplate(color), function (err) { + assert.ifError(err); + + getNamedTile(templateId, function(err, res, img) { + assert.ifError(err); + + assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, function (err) { + assert.ifError(err); + + deleteTemplate(templateId, function (err) { + assert.ifError(err); + + getNamedTile(templateId, { statusCode: 404 }, function(err, res) { + assert.ifError(err); + + assert.deepEqual( + JSON.parse(res.body).errors, + ["Template 'template_with_color_black' of user 'localhost' not found"] + ); + + done(); + }); + }); + }); + }); + }); + }); }); From 3d2f554be9667d888b808ca4a674b28fc0b52dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 16:53:53 +0200 Subject: [PATCH 04/18] Use early return pattern --- lib/cartodb/cache/named_map_provider_cache.js | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index e5f63d63..c65a4bca 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -32,31 +32,29 @@ module.exports = NamedMapProviderCache; NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) { var namedMapKey = createNamedMapKey(user, templateId); var namedMapProviders = this.providerCache.get(namedMapKey) || {}; - var providerKey = createProviderKey(config, authToken, params); - if (!namedMapProviders.hasOwnProperty(providerKey)) { - namedMapProviders[providerKey] = new NamedMapMapConfigProvider( - this.templateMaps, - this.pgConnection, - this.metadataBackend, - this.userLimitsBackend, - this.mapConfigAdapter, - this.affectedTablesCache, - user, - templateId, - config, - authToken, - params - ); - this.providerCache.set(namedMapKey, namedMapProviders); - // early exit, if provider did not exist we just return it + if (namedMapProviders.hasOwnProperty(providerKey)) { return callback(null, namedMapProviders[providerKey]); } - var namedMapProvider = namedMapProviders[providerKey]; + namedMapProviders[providerKey] = new NamedMapMapConfigProvider( + this.templateMaps, + this.pgConnection, + this.metadataBackend, + this.userLimitsBackend, + this.mapConfigAdapter, + this.affectedTablesCache, + user, + templateId, + config, + authToken, + params + ); - return callback(null, namedMapProvider); + this.providerCache.set(namedMapKey, namedMapProviders); + + return callback(null, namedMapProviders[providerKey]); }; NamedMapProviderCache.prototype.invalidate = function(user, templateId) { From 1bb6a2ac0d53998fef6d28d21ff06173689376c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 17:42:56 +0200 Subject: [PATCH 05/18] Move invalidation closer to its definition --- lib/cartodb/api/api-router.js | 4 ---- lib/cartodb/cache/named_map_provider_cache.js | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/cartodb/api/api-router.js b/lib/cartodb/api/api-router.js index 14c7acec..60946a78 100644 --- a/lib/cartodb/api/api-router.js +++ b/lib/cartodb/api/api-router.js @@ -161,10 +161,6 @@ module.exports = class ApiRouter { layergroupAffectedTablesCache ); - ['update', 'delete'].forEach(function(eventType) { - templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache)); - }); - const collaborators = { analysisStatusBackend, attributesBackend, diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index c65a4bca..94ca074c 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -8,6 +8,7 @@ var templateName = require('../backends/template_maps').templateName; var LruCache = require("lru-cache"); const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10; +const ACTIONS = ['update', 'delete']; function NamedMapProviderCache( templateMaps, @@ -25,6 +26,8 @@ function NamedMapProviderCache( this.affectedTablesCache = affectedTablesCache; this.providerCache = new LruCache({ max: 2000, maxAge: TEN_MINUTES_IN_MILLISECONDS }); + + ACTIONS.forEach(action => templateMaps.on(action, (...args) => this.invalidate(...args))); } module.exports = NamedMapProviderCache; From c0afd42fa21896364f6c3442dfa2958d92b6d9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 17:53:10 +0200 Subject: [PATCH 06/18] Use template strings instead of dot module --- lib/cartodb/cache/named_map_provider_cache.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index 94ca074c..0160eb20 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -1,7 +1,6 @@ 'use strict'; var _ = require('underscore'); -var dot = require('dot'); var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); var templateName = require('../backends/template_maps').templateName; @@ -68,8 +67,7 @@ function createNamedMapKey(user, templateId) { return user + ':' + templateName(templateId); } -var providerKey = '{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}'; -var providerKeyTpl = dot.template(providerKey); +var providerKeyTpl = ctx => `${ctx.authToken}:${ctx.configHash}:${ctx.format}:${ctx.layer}:${ctx.scale_factor}`; function createProviderKey(config, authToken, params) { var tplValues = _.defaults({}, params, { From e8d5e42300e185b2fb8b7e82e28ef6589a46a15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 17:58:23 +0200 Subject: [PATCH 07/18] Use Object.assign() instead of _.defaults() --- lib/cartodb/cache/named_map_provider_cache.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index 0160eb20..de879c73 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -1,6 +1,5 @@ 'use strict'; -var _ = require('underscore'); var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); var templateName = require('../backends/template_maps').templateName; @@ -70,12 +69,14 @@ function createNamedMapKey(user, templateId) { var providerKeyTpl = ctx => `${ctx.authToken}:${ctx.configHash}:${ctx.format}:${ctx.layer}:${ctx.scale_factor}`; function createProviderKey(config, authToken, params) { - var tplValues = _.defaults({}, params, { + const defaults = { authToken: authToken || '', configHash: NamedMapMapConfigProvider.configHash(config), layer: '', format: '', scale_factor: 1 - }); - return providerKeyTpl(tplValues); + }; + const ctx = Object.assign({}, defaults, params); + + return providerKeyTpl(ctx); } From d99e5a44f524fc1f47517ba1d32400a0aa6ea65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 18:00:13 +0200 Subject: [PATCH 08/18] Use const instead of var --- lib/cartodb/cache/named_map_provider_cache.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index de879c73..c872b49d 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -1,9 +1,9 @@ 'use strict'; -var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); -var templateName = require('../backends/template_maps').templateName; +const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); +const templateName = require('../backends/template_maps').templateName; -var LruCache = require("lru-cache"); +const LruCache = require("lru-cache"); const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10; const ACTIONS = ['update', 'delete']; @@ -31,9 +31,9 @@ function NamedMapProviderCache( module.exports = NamedMapProviderCache; NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) { - var namedMapKey = createNamedMapKey(user, templateId); - var namedMapProviders = this.providerCache.get(namedMapKey) || {}; - var providerKey = createProviderKey(config, authToken, params); + const namedMapKey = createNamedMapKey(user, templateId); + const namedMapProviders = this.providerCache.get(namedMapKey) || {}; + const providerKey = createProviderKey(config, authToken, params); if (namedMapProviders.hasOwnProperty(providerKey)) { return callback(null, namedMapProviders[providerKey]); @@ -66,7 +66,7 @@ function createNamedMapKey(user, templateId) { return user + ':' + templateName(templateId); } -var providerKeyTpl = ctx => `${ctx.authToken}:${ctx.configHash}:${ctx.format}:${ctx.layer}:${ctx.scale_factor}`; +const providerKeyTpl = ctx => `${ctx.authToken}:${ctx.configHash}:${ctx.format}:${ctx.layer}:${ctx.scale_factor}`; function createProviderKey(config, authToken, params) { const defaults = { From b55c2ec55c710329a969c1954b3316e52cb34783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 18:03:39 +0200 Subject: [PATCH 09/18] Use template string --- lib/cartodb/cache/named_map_provider_cache.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index c872b49d..fbb01509 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -1,10 +1,10 @@ 'use strict'; -const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); -const templateName = require('../backends/template_maps').templateName; - const LruCache = require("lru-cache"); +const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); +const { templateName } = require('../backends/template_maps'); + const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10; const ACTIONS = ['update', 'delete']; @@ -62,13 +62,13 @@ NamedMapProviderCache.prototype.invalidate = function(user, templateId) { this.providerCache.del(createNamedMapKey(user, templateId)); }; -function createNamedMapKey(user, templateId) { - return user + ':' + templateName(templateId); +function createNamedMapKey (user, templateId) { + return `${user}:${templateName(templateId)}`; } const providerKeyTpl = ctx => `${ctx.authToken}:${ctx.configHash}:${ctx.format}:${ctx.layer}:${ctx.scale_factor}`; -function createProviderKey(config, authToken, params) { +function createProviderKey (config, authToken, params) { const defaults = { authToken: authToken || '', configHash: NamedMapMapConfigProvider.configHash(config), From 329b5d9b9e6a9d8cf4658f2bab0216402d5770a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 18:07:50 +0200 Subject: [PATCH 10/18] Use ES6 class syntax --- lib/cartodb/cache/named_map_provider_cache.js | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index fbb01509..5883ce07 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -8,58 +8,58 @@ const { templateName } = require('../backends/template_maps'); const TEN_MINUTES_IN_MILLISECONDS = 1000 * 60 * 10; const ACTIONS = ['update', 'delete']; -function NamedMapProviderCache( - templateMaps, - pgConnection, - metadataBackend, - userLimitsBackend, - mapConfigAdapter, - affectedTablesCache -) { - this.templateMaps = templateMaps; - this.pgConnection = pgConnection; - this.metadataBackend = metadataBackend; - this.userLimitsBackend = userLimitsBackend; - this.mapConfigAdapter = mapConfigAdapter; - this.affectedTablesCache = affectedTablesCache; +module.exports = class NamedMapProviderCache { + constructor ( + templateMaps, + pgConnection, + metadataBackend, + userLimitsBackend, + mapConfigAdapter, + affectedTablesCache + ) { + this.templateMaps = templateMaps; + this.pgConnection = pgConnection; + this.metadataBackend = metadataBackend; + this.userLimitsBackend = userLimitsBackend; + this.mapConfigAdapter = mapConfigAdapter; + this.affectedTablesCache = affectedTablesCache; - this.providerCache = new LruCache({ max: 2000, maxAge: TEN_MINUTES_IN_MILLISECONDS }); + this.providerCache = new LruCache({ max: 2000, maxAge: TEN_MINUTES_IN_MILLISECONDS }); - ACTIONS.forEach(action => templateMaps.on(action, (...args) => this.invalidate(...args))); -} + ACTIONS.forEach(action => templateMaps.on(action, (...args) => this.invalidate(...args))); + } -module.exports = NamedMapProviderCache; + get (user, templateId, config, authToken, params, callback) { + const namedMapKey = createNamedMapKey(user, templateId); + const namedMapProviders = this.providerCache.get(namedMapKey) || {}; + const providerKey = createProviderKey(config, authToken, params); -NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) { - const namedMapKey = createNamedMapKey(user, templateId); - const namedMapProviders = this.providerCache.get(namedMapKey) || {}; - const providerKey = createProviderKey(config, authToken, params); + if (namedMapProviders.hasOwnProperty(providerKey)) { + return callback(null, namedMapProviders[providerKey]); + } + + namedMapProviders[providerKey] = new NamedMapMapConfigProvider( + this.templateMaps, + this.pgConnection, + this.metadataBackend, + this.userLimitsBackend, + this.mapConfigAdapter, + this.affectedTablesCache, + user, + templateId, + config, + authToken, + params + ); + + this.providerCache.set(namedMapKey, namedMapProviders); - if (namedMapProviders.hasOwnProperty(providerKey)) { return callback(null, namedMapProviders[providerKey]); } - namedMapProviders[providerKey] = new NamedMapMapConfigProvider( - this.templateMaps, - this.pgConnection, - this.metadataBackend, - this.userLimitsBackend, - this.mapConfigAdapter, - this.affectedTablesCache, - user, - templateId, - config, - authToken, - params - ); - - this.providerCache.set(namedMapKey, namedMapProviders); - - return callback(null, namedMapProviders[providerKey]); -}; - -NamedMapProviderCache.prototype.invalidate = function(user, templateId) { - this.providerCache.del(createNamedMapKey(user, templateId)); + invalidate (user, templateId) { + this.providerCache.del(createNamedMapKey(user, templateId)); + } }; function createNamedMapKey (user, templateId) { From 824d41ef0fe05fc561aa253cc9d88bea6c4b279a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 18:11:47 +0200 Subject: [PATCH 11/18] Consitent quotes --- lib/cartodb/cache/named_map_provider_cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index 5883ce07..e738ede3 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -1,6 +1,6 @@ 'use strict'; -const LruCache = require("lru-cache"); +const LruCache = require('lru-cache'); const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); const { templateName } = require('../backends/template_maps'); From 788cd9d6fb105f8bc9cb3b36d03087f85a26bdb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 18:15:11 +0200 Subject: [PATCH 12/18] Be explicit while forwarding parametes --- lib/cartodb/cache/named_map_provider_cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index e738ede3..ea24f2f5 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -26,7 +26,7 @@ module.exports = class NamedMapProviderCache { this.providerCache = new LruCache({ max: 2000, maxAge: TEN_MINUTES_IN_MILLISECONDS }); - ACTIONS.forEach(action => templateMaps.on(action, (...args) => this.invalidate(...args))); + ACTIONS.forEach(action => templateMaps.on(action, (user, templateId) => this.invalidate(user, templateId))); } get (user, templateId, config, authToken, params, callback) { From c19c7237952dd729c85febf336c9df41295822cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Fri, 13 Sep 2019 20:01:03 +0200 Subject: [PATCH 13/18] Add named map providers reporter to gather some stats --- lib/cartodb/api/api-router.js | 8 +++ .../stats/reporter/named-map-provider.js | 33 ++++++++++ .../stats/reporter/named-map-provider.js | 61 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 lib/cartodb/stats/reporter/named-map-provider.js create mode 100644 test/unit/cartodb/stats/reporter/named-map-provider.js diff --git a/lib/cartodb/api/api-router.js b/lib/cartodb/api/api-router.js index 60946a78..fb62aa46 100644 --- a/lib/cartodb/api/api-router.js +++ b/lib/cartodb/api/api-router.js @@ -29,6 +29,7 @@ const VarnishHttpCacheBackend = require('../cache/backend/varnish_http'); const FastlyCacheBackend = require('../cache/backend/fastly'); const NamedMapProviderCache = require('../cache/named_map_provider_cache'); const NamedMapsCacheEntry = require('../cache/model/named_maps_entry'); +const NamedMapProviderReporter = require('../stats/reporter/named-map-provider'); const SqlWrapMapConfigAdapter = require('../models/mapconfig/adapter/sql-wrap-mapconfig-adapter'); const MapConfigNamedLayersAdapter = require('../models/mapconfig/adapter/mapconfig-named-layers-adapter'); @@ -161,6 +162,13 @@ module.exports = class ApiRouter { layergroupAffectedTablesCache ); + const namedMapProviderReporter = new NamedMapProviderReporter({ + namedMapProviderCache, + intervalInMilliseconds: rendererCacheOpts.statsInterval + }); + + namedMapProviderReporter.start(); + const collaborators = { analysisStatusBackend, attributesBackend, diff --git a/lib/cartodb/stats/reporter/named-map-provider.js b/lib/cartodb/stats/reporter/named-map-provider.js new file mode 100644 index 00000000..c115b716 --- /dev/null +++ b/lib/cartodb/stats/reporter/named-map-provider.js @@ -0,0 +1,33 @@ +'use strict'; + +const statKeyTemplate = ctx => `windshaft.named-map-provider-cache.${ctx.metric}`; + +module.exports = class NamedMapProviderReporter { + constructor ({ namedMapProviderCache, intervalInMilliseconds } = {}) { + this.namedMapProviderCache = namedMapProviderCache; + this.intervalInMilliseconds = intervalInMilliseconds; + this.intervalId = null; + } + + start () { + const { providerCache: cache } = this.namedMapProviderCache; + const { statsClient: stats } = global; + + this.intervalId = setInterval(() => { + stats.gauge(statKeyTemplate({ metric: 'named-map.count' }), cache.length); + const providers = cache.dump(); + + const namedMapIntantiations = providers.reduce((acc, { v: providers }) => { + acc += Object.keys(providers).length; + return acc; + }, 0); + + stats.gauge(statKeyTemplate({ metric: 'named-map.intantiation.count' }), namedMapIntantiations); + }, this.intervalInMilliseconds); + } + + stop () { + clearInterval(this.intervalId); + this.intervalId = null; + } +}; diff --git a/test/unit/cartodb/stats/reporter/named-map-provider.js b/test/unit/cartodb/stats/reporter/named-map-provider.js new file mode 100644 index 00000000..551a1c21 --- /dev/null +++ b/test/unit/cartodb/stats/reporter/named-map-provider.js @@ -0,0 +1,61 @@ +'use strict'; + +const assert = require('assert'); +const NamedMapProviderReporter = require('../../../../../lib/cartodb/stats/reporter/named-map-provider'); + +describe('named-map-provider-reporter', function () { + it('should report metrics every 100 ms', function (done) { + const oldStatsClient = global.statsClient; + + global.statsClient = { + gauge: function (metric, value) { + this[metric] = value; + } + }; + + const dummyCacheEntries = [ + { + k: 'foo:template_1', + v: { 'instantiation_1': 1 } + }, + { + k: 'bar:template_2', + v: { 'instantiation_1': 1, 'instantiation_2': 2 } + }, + { + k: 'buz:template_3', + v: { 'instantiation_1': 1, 'instantiation_2': 2, 'instantiation_3': 3 } + } + ]; + + const reporter = new NamedMapProviderReporter({ + namedMapProviderCache: { + providerCache: { + dump: () => dummyCacheEntries, + length: dummyCacheEntries.length + } + }, + intervalInMilliseconds: 100 + }); + + reporter.start(); + + setTimeout(() => { + reporter.stop(); + + assert.strictEqual( + global.statsClient['windshaft.named-map-provider-cache.named-map.count'], + 3 + ); + + assert.strictEqual( + global.statsClient['windshaft.named-map-provider-cache.named-map.intantiation.count'], + 6 + ); + + global.statsClient = oldStatsClient; + + done(); + }, 110); + }); +}); From 5dac9d956cf8a7cd037bed4b74b10dfa6a67a451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Mon, 16 Sep 2019 11:18:50 +0200 Subject: [PATCH 14/18] Add test --- .../acceptance/named-map-cache-regressions.js | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 test/acceptance/named-map-cache-regressions.js diff --git a/test/acceptance/named-map-cache-regressions.js b/test/acceptance/named-map-cache-regressions.js new file mode 100644 index 00000000..52040cfe --- /dev/null +++ b/test/acceptance/named-map-cache-regressions.js @@ -0,0 +1,231 @@ +'use strict'; + +require('../support/test_helper'); + +const request = require('request'); +const assert = require('assert'); +const Server = require('../../lib/cartodb/server'); +const serverOptions = require('../../lib/cartodb/server_options'); +const { mapnik } = require('windshaft'); +const helper = require('../support/test_helper'); + +describe('named map cache regressions', function () { + const server = new Server(serverOptions); + + const apiKey = 1234; + + const template = { + version: '0.0.1', + name: 'named-map-cache-regression-missing-template', + layergroup: { + version: '1.8.0', + layers: [ + { + type: 'cartodb', + options: { + source: { + id: 'a1' + }, + cartocss: '#layer{marker-placement: point;marker-width: 5;marker-fill: red;}', + cartocss_version: '2.3.0' + } + } + ], + analyses: [ + { + id: 'a1', + type: 'source', + params: { + query: 'select * from populated_places_simple_reduced' + } + } + ] + } + }; + + const port = 0; // let the OS to choose a free port + const host = '127.0.0.1'; + + let listener; + let address; + + const keysToDelete = {}; + + before(function (done) { + listener = server.listen(port, host); + + listener.on('error', done); + listener.on('listening', () => { + const { address: host, port } = listener.address(); + + address = `${host}:${port}`; + + done(); + }); + }); + + after(function (done) { + helper.deleteRedisKeys(keysToDelete, () => listener.close(done)); + }); + + it('should not fail when a template gets recreated', function (done) { + this.timeout(10000); + + const createTemplateRequest = { + url: `http://${address}/api/v1/map/named?api_key=${apiKey}`, + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + body: template, + json: true + }; + + request(createTemplateRequest, (err, res, body) => { + if (err) { + return done(err); + } + + assert.strictEqual(res.statusCode, 200); + + const templateId = body.template_id; + + keysToDelete['map_tpl|localhost'] = 0; + + const previewRequest = { + url: `http://${address}/api/v1/map/static/named/${templateId}/256/256.png?api_key=${apiKey}`, + encoding: 'binary', + method: 'GET', + headers: { + host: 'localhost' + } + }; + + request(previewRequest, (err, res) => { + if (err) { + return done(err); + } + + assert.strictEqual(res.statusCode, 200); + + const preview = mapnik.Image.fromBytes(Buffer.from(res.body, 'binary')); + + assert.strictEqual(preview.width(), 256); + assert.strictEqual(preview.height(), 256); + + const templateUpdate = Object.assign({}, template); + + templateUpdate.layergroup.analyses[0].params.query = 'select * from populated_places_simple_reduced limit 100'; + + const upateTemplateRequest = { + url: `http://${address}/api/v1/map/named/${templateId}?api_key=${apiKey}`, + method: 'PUT', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + body: templateUpdate, + json: true + }; + + request(upateTemplateRequest, (err, res, body) => { + if (err) { + return done(err); + } + + assert.strictEqual(res.statusCode, 200); + + request(previewRequest, (err, res) => { + if (err) { + return done(err); + } + + const preview = mapnik.Image.fromBytes(Buffer.from(res.body, 'binary')); + + assert.strictEqual(preview.width(), 256); + assert.strictEqual(preview.height(), 256); + + request(previewRequest, (err, res) => { + if (err) { + return done(err); + } + + const preview = mapnik.Image.fromBytes(Buffer.from(res.body, 'binary')); + + assert.strictEqual(preview.width(), 256); + assert.strictEqual(preview.height(), 256); + + const deleteTemplateRequest = { + url: `http://${address}/api/v1/map/named/${templateId}?api_key=${apiKey}`, + method: 'DELETE', + headers: { + host: 'localhost', + } + }; + + request(deleteTemplateRequest, (err) => { + if (err) { + return done(err); + } + + delete keysToDelete['map_tpl|localhost']; + + assert.strictEqual(res.statusCode, 200); + + request(createTemplateRequest, (err, res, body) => { + if (err) { + return done(err); + } + + assert.strictEqual(res.statusCode, 200); + + const templateId = body.template_id; + + keysToDelete['map_tpl|localhost'] = 0; + + const previewRequest = { + url: `http://${address}/api/v1/map/static/named/${templateId}/256/256.png?api_key=${apiKey}`, + encoding: 'binary', + method: 'GET', + headers: { + host: 'localhost' + } + }; + + request(previewRequest, (err, res) => { + if (err) { + return done(err); + } + + assert.strictEqual(res.statusCode, 200); + + const preview = mapnik.Image.fromBytes(Buffer.from(res.body, 'binary')); + + assert.strictEqual(preview.width(), 256); + assert.strictEqual(preview.height(), 256); + + request(deleteTemplateRequest, (err) => { + if (err) { + return done(err); + } + + delete keysToDelete['map_tpl|localhost']; + + assert.strictEqual(res.statusCode, 200); + + keysToDelete['user:localhost:mapviews:global'] = 0; + keysToDelete['user:localhost:mapviews:global'] = 5; + + helper.deleteRedisKeys(keysToDelete, done); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); From 738d10409fe64c567920fd8e87bf92ce47fa5c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Mon, 16 Sep 2019 11:35:40 +0200 Subject: [PATCH 15/18] Style --- test/acceptance/named-map-cache-regressions.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/acceptance/named-map-cache-regressions.js b/test/acceptance/named-map-cache-regressions.js index 52040cfe..23cd7b74 100644 --- a/test/acceptance/named-map-cache-regressions.js +++ b/test/acceptance/named-map-cache-regressions.js @@ -9,6 +9,10 @@ const serverOptions = require('../../lib/cartodb/server_options'); const { mapnik } = require('windshaft'); const helper = require('../support/test_helper'); +const namedTileUrlTemplate = (ctx) => { + return `http://${ctx.address}/api/v1/map/static/named/${ctx.templateId}/256/256.png?api_key=${ctx.apiKey}`; +}; + describe('named map cache regressions', function () { const server = new Server(serverOptions); @@ -116,7 +120,8 @@ describe('named map cache regressions', function () { const templateUpdate = Object.assign({}, template); - templateUpdate.layergroup.analyses[0].params.query = 'select * from populated_places_simple_reduced limit 100'; + const newQuery = 'select * from populated_places_simple_reduced limit 100'; + templateUpdate.layergroup.analyses[0].params.query = newQuery; const upateTemplateRequest = { url: `http://${address}/api/v1/map/named/${templateId}?api_key=${apiKey}`, @@ -129,7 +134,7 @@ describe('named map cache regressions', function () { json: true }; - request(upateTemplateRequest, (err, res, body) => { + request(upateTemplateRequest, (err, res) => { if (err) { return done(err); } @@ -185,7 +190,7 @@ describe('named map cache regressions', function () { keysToDelete['map_tpl|localhost'] = 0; const previewRequest = { - url: `http://${address}/api/v1/map/static/named/${templateId}/256/256.png?api_key=${apiKey}`, + url: namedTileUrlTemplate({ address, templateId, apiKey }), encoding: 'binary', method: 'GET', headers: { From f6c205baf9d30e81c2e75686376901283e3137eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Mon, 16 Sep 2019 13:08:10 +0200 Subject: [PATCH 16/18] Typo --- lib/cartodb/stats/reporter/named-map-provider.js | 4 ++-- test/acceptance/named-map-cache-regressions.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/cartodb/stats/reporter/named-map-provider.js b/lib/cartodb/stats/reporter/named-map-provider.js index c115b716..875827cd 100644 --- a/lib/cartodb/stats/reporter/named-map-provider.js +++ b/lib/cartodb/stats/reporter/named-map-provider.js @@ -17,12 +17,12 @@ module.exports = class NamedMapProviderReporter { stats.gauge(statKeyTemplate({ metric: 'named-map.count' }), cache.length); const providers = cache.dump(); - const namedMapIntantiations = providers.reduce((acc, { v: providers }) => { + const namedMapInstantiations = providers.reduce((acc, { v: providers }) => { acc += Object.keys(providers).length; return acc; }, 0); - stats.gauge(statKeyTemplate({ metric: 'named-map.intantiation.count' }), namedMapIntantiations); + stats.gauge(statKeyTemplate({ metric: 'named-map.intantiation.count' }), namedMapInstantiations); }, this.intervalInMilliseconds); } diff --git a/test/acceptance/named-map-cache-regressions.js b/test/acceptance/named-map-cache-regressions.js index 23cd7b74..8521ca8a 100644 --- a/test/acceptance/named-map-cache-regressions.js +++ b/test/acceptance/named-map-cache-regressions.js @@ -123,7 +123,7 @@ describe('named map cache regressions', function () { const newQuery = 'select * from populated_places_simple_reduced limit 100'; templateUpdate.layergroup.analyses[0].params.query = newQuery; - const upateTemplateRequest = { + const updateTemplateRequest = { url: `http://${address}/api/v1/map/named/${templateId}?api_key=${apiKey}`, method: 'PUT', headers: { @@ -134,7 +134,7 @@ describe('named map cache regressions', function () { json: true }; - request(upateTemplateRequest, (err, res) => { + request(updateTemplateRequest, (err, res) => { if (err) { return done(err); } From 68a9b4ccae3bbf5b64b9fb26af6ac9b45044eab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Mon, 16 Sep 2019 13:09:23 +0200 Subject: [PATCH 17/18] Typo --- lib/cartodb/stats/reporter/named-map-provider.js | 2 +- test/unit/cartodb/stats/reporter/named-map-provider.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cartodb/stats/reporter/named-map-provider.js b/lib/cartodb/stats/reporter/named-map-provider.js index 875827cd..c057e012 100644 --- a/lib/cartodb/stats/reporter/named-map-provider.js +++ b/lib/cartodb/stats/reporter/named-map-provider.js @@ -22,7 +22,7 @@ module.exports = class NamedMapProviderReporter { return acc; }, 0); - stats.gauge(statKeyTemplate({ metric: 'named-map.intantiation.count' }), namedMapInstantiations); + stats.gauge(statKeyTemplate({ metric: 'named-map.instantiation.count' }), namedMapInstantiations); }, this.intervalInMilliseconds); } diff --git a/test/unit/cartodb/stats/reporter/named-map-provider.js b/test/unit/cartodb/stats/reporter/named-map-provider.js index 551a1c21..f99a9edf 100644 --- a/test/unit/cartodb/stats/reporter/named-map-provider.js +++ b/test/unit/cartodb/stats/reporter/named-map-provider.js @@ -49,7 +49,7 @@ describe('named-map-provider-reporter', function () { ); assert.strictEqual( - global.statsClient['windshaft.named-map-provider-cache.named-map.intantiation.count'], + global.statsClient['windshaft.named-map-provider-cache.named-map.instantiation.count'], 6 ); From 455932b032f7dd778582ca06ad0ede9ebd9076f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Mon, 16 Sep 2019 13:26:50 +0200 Subject: [PATCH 18/18] Update NEWS --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index ea9f57c0..4b4fa0b0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,8 @@ Released 2019-XX-XX Announcements: +- Stop caching map template errors in Named Map Provider Cache +- Gather metrics from Named Maps Providers Cache - Improved efficiency of query samples while instatiating a map (#1120). - Cache control header fine tuning. Set a shorter value for "max-age" directive if there is no way to know when to trigger the invalidation. - Update deps: