diff --git a/lib/cartodb/cache/named_map_provider_cache.js b/lib/cartodb/cache/named_map_provider_cache.js index 44afc98e..1da6cf15 100644 --- a/lib/cartodb/cache/named_map_provider_cache.js +++ b/lib/cartodb/cache/named_map_provider_cache.js @@ -2,6 +2,7 @@ var _ = require('underscore'); var dot = require('dot'); var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider'); var templateName = require('../backends/template_maps').templateName; +var queue = require('queue-async'); var LruCache = require("lru-cache"); @@ -34,9 +35,30 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok params ); this.providerCache.set(namedMapKey, namedMapProviders); + + // early exit, if provider did not exist we just return it + return callback(null, namedMapProviders[providerKey]); } - return callback(null, namedMapProviders[providerKey]); + 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); + }); }; NamedMapProviderCache.prototype.invalidate = function(user, templateId) { diff --git a/lib/cartodb/models/mapconfig/named_map_provider.js b/lib/cartodb/models/mapconfig/named_map_provider.js index f1565c66..cea4ff39 100644 --- a/lib/cartodb/models/mapconfig/named_map_provider.js +++ b/lib/cartodb/models/mapconfig/named_map_provider.js @@ -152,6 +152,17 @@ NamedMapMapConfigProvider.prototype.getCacheBuster = function() { return this.cacheBuster; }; +NamedMapMapConfigProvider.prototype.reset = function() { + this.template = null; + + this.affectedTablesAndLastUpdate = null; + + this.err = null; + this.mapConfig = null; + + this.cacheBuster = Date.now(); +}; + NamedMapMapConfigProvider.prototype.filter = function(key) { var regex = new RegExp('^' + this.createKey(true) + '.*'); return key && key.match(regex); diff --git a/test/acceptance/named_maps_cache.js b/test/acceptance/named_maps_cache.js new file mode 100644 index 00000000..a2f2295e --- /dev/null +++ b/test/acceptance/named_maps_cache.js @@ -0,0 +1,140 @@ +require('../support/test_helper'); +var RedisPool = require('redis-mpool'); + +var assert = require('../support/assert'); +var mapnik = require('windshaft').mapnik; +var CartodbWindshaft = require('../../lib/cartodb/server'); +var serverOptions = require('../../lib/cartodb/server_options'); +var server = new CartodbWindshaft(serverOptions); +var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js'); + +describe('named maps provider cache', function() { + // 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'; + var templateName = 'template_with_color'; + + var IMAGE_TOLERANCE = 20; + + function createTemplate(color) { + return { + version: '0.0.1', + name: templateName, + auth: { + method: 'open' + }, + placeholders: { + color: { + type: "css_color", + default: color + } + }, + layergroup: { + layers: [ + { + type: 'mapnik', + options: { + sql: 'select * from populated_places_simple_reduced', + cartocss: '#layer { marker-fill: <%= color %>; marker-line-color: <%= color %>; }', + cartocss_version: '2.3.0' + } + } + ] + } + }; + } + + afterEach(function (done) { + templateMaps.delTemplate(username, templateName, done); + }); + + function getNamedTile(options, callback) { + if (!callback) { + callback = options; + options = {}; + } + + var url = '/api/v1/map/named/' + templateName + '/all/' + [0,0,0].join('/') + '.png'; + + var requestOptions = { + url: url, + method: 'GET', + headers: { + host: username + }, + encoding: 'binary' + }; + + var statusCode = options.statusCode || 200; + + var expectedResponse = { + status: statusCode, + headers: { + 'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8' + } + }; + + assert.response(server, requestOptions, expectedResponse, function (res, err) { + var img; + if (statusCode === 200) { + img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary')); + } + return callback(err, res, img); + }); + } + + function previewFixture(color) { + return './test/fixtures/provider/populated_places_simple_reduced-' + color + '.png'; + } + + var colors = ['red', '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) { + if (err) { + return done(err); + } + getNamedTile(function(err, res, img) { + assert.ok(!err); + assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, done); + }); + }); + }); + }); + + 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); + }); + }); + }); + }); + }); + }); + +}); diff --git a/test/fixtures/provider/populated_places_simple_reduced-black.png b/test/fixtures/provider/populated_places_simple_reduced-black.png new file mode 100644 index 00000000..e3cf10c3 Binary files /dev/null and b/test/fixtures/provider/populated_places_simple_reduced-black.png differ diff --git a/test/fixtures/provider/populated_places_simple_reduced-blue.png b/test/fixtures/provider/populated_places_simple_reduced-blue.png new file mode 100644 index 00000000..647e43bf Binary files /dev/null and b/test/fixtures/provider/populated_places_simple_reduced-blue.png differ diff --git a/test/fixtures/provider/populated_places_simple_reduced-green.png b/test/fixtures/provider/populated_places_simple_reduced-green.png new file mode 100644 index 00000000..b7e5cc69 Binary files /dev/null and b/test/fixtures/provider/populated_places_simple_reduced-green.png differ diff --git a/test/fixtures/provider/populated_places_simple_reduced-red.png b/test/fixtures/provider/populated_places_simple_reduced-red.png new file mode 100644 index 00000000..3219792e Binary files /dev/null and b/test/fixtures/provider/populated_places_simple_reduced-red.png differ