diff --git a/lib/cartodb/cache/backend/varnish_http.js b/lib/cartodb/cache/backend/varnish_http.js new file mode 100644 index 00000000..25ff2745 --- /dev/null +++ b/lib/cartodb/cache/backend/varnish_http.js @@ -0,0 +1,32 @@ +var request = require('request'); + +function VarnishHttpCacheBackend(host, port) { + this.host = host; + this.port = port; +} + +module.exports = VarnishHttpCacheBackend; + +/** + * @param cacheObject should respond to `key() -> String` method + * @param {Function} callback + */ +VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) { + request( + { + method: 'PURGE', + url: 'http://' + this.host + ':' + this.port + '/key', + headers: { + 'Invalidation-Match': '\b' + cacheObject.key() + '\b' + } + }, + function(err, response) { + if (err || response.statusCode !== 204) { + return callback(new Error('Unable to invalidate Varnish object')); + } + return callback(null); + } + ); +}; + +module.exports = VarnishHttpCacheBackend; \ No newline at end of file diff --git a/lib/cartodb/cache/model/named_maps_entry.js b/lib/cartodb/cache/model/named_maps_entry.js new file mode 100644 index 00000000..aaab7f3a --- /dev/null +++ b/lib/cartodb/cache/model/named_maps_entry.js @@ -0,0 +1,18 @@ +var crypto = require('crypto'); + +function NamedMaps(owner, name) { + this.namespace = 'n'; + this.owner = owner; + this.name = name; +} + +module.exports = NamedMaps; + + +NamedMaps.prototype.key = function() { + return this.namespace + ':' + shortHashKey(this.owner + ':' + this.name); +}; + +function shortHashKey(target) { + return crypto.createHash('sha256').update(target).digest('base64').substring(0,6); +} diff --git a/lib/cartodb/cache/surrogate_keys_cache.js b/lib/cartodb/cache/surrogate_keys_cache.js new file mode 100644 index 00000000..53b9f45a --- /dev/null +++ b/lib/cartodb/cache/surrogate_keys_cache.js @@ -0,0 +1,26 @@ +/** + * @param cacheBackend should respond to `invalidate(cacheObject, callback)` method + * @constructor + */ +function SurrogateKeysCache(cacheBackend) { + this.cacheBackend = cacheBackend; +} + +module.exports = SurrogateKeysCache; + + +/** + * @param response should respond to `header(key, value)` method + * @param cacheObject should respond to `key() -> String` method + */ +SurrogateKeysCache.prototype.tag = function(response, cacheObject) { + response.header('Surrogate-Key', cacheObject.key()); +}; + +/** + * @param cacheObject should respond to `key() -> String` method + * @param {Function} callback + */ +SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) { + this.cacheBackend.invalidate(cacheObject, callback); +}; diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index 46a855f7..0a42ff06 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -25,15 +25,6 @@ var CartodbWindshaft = function(serverOptions) { var cartoData = require('cartodb-redis')({pool: redisPool}); - if(serverOptions.cache_enabled) { - console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port); - Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret); - serverOptions.afterStateChange = function(req, data, callback) { - Cache.invalidate_db(req.params.dbname, req.params.table); - callback(null, data); - } - } - serverOptions.beforeStateChange = function(req, callback) { var err = null; if ( ! req.params.hasOwnProperty('_authorizedByApiKey') ) { @@ -54,6 +45,35 @@ var CartodbWindshaft = function(serverOptions) { var templateMaps = new TemplateMaps(redisPool, templateMapsOpts); serverOptions.templateMaps = templateMaps; + var SurrogateKeysCache = require('./cache/surrogate_keys_cache'), + NamedMapsCacheEntry = require('./cache/model/named_maps_entry'), + VarnishHttpCacheBackend = require('./cache/backend/varnish_http'), + varnishHttpCacheBackend = new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port), + surrogateKeysCache = new SurrogateKeysCache(varnishHttpCacheBackend); + + if (serverOptions.cache_enabled) { + + console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port); + + Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret); + serverOptions.afterStateChange = function(req, data, callback) { + Cache.invalidate_db(req.params.dbname, req.params.table); + callback(null, data); + }; + + function invalidateNamedMap(owner, templateName) { + surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) { + if (err) { + console.warn('Cache: surrogate key invalidation failed'); + } + }); + } + + ['update', 'delete'].forEach(function(eventType) { + templateMaps.on(eventType, invalidateNamedMap); + }); + } + // boot var ws = new Windshaft.Server(serverOptions); @@ -138,7 +158,7 @@ var CartodbWindshaft = function(serverOptions) { var TemplateMapsController = require('./controllers/template_maps'), templateMapsController = new TemplateMapsController( - ws, serverOptions, templateMaps, cartoData, template_baseurl + ws, serverOptions, templateMaps, cartoData, template_baseurl, surrogateKeysCache, NamedMapsCacheEntry ); templateMapsController.register(ws); diff --git a/lib/cartodb/controllers/template_maps.js b/lib/cartodb/controllers/template_maps.js index 3ee87bf9..db525c7c 100644 --- a/lib/cartodb/controllers/template_maps.js +++ b/lib/cartodb/controllers/template_maps.js @@ -1,12 +1,15 @@ var Step = require('step'); var _ = require('underscore'); -function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl) { +function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache, + NamedMapsCacheEntry) { this.app = app; this.serverOptions = serverOptions; this.templateMaps = templateMaps; this.metadataBackend = metadataBackend; this.templateBaseUrl = templateBaseUrl; + this.surrogateKeysCache = surrogateKeysCache; + this.NamedMapsCacheEntry = NamedMapsCacheEntry; } module.exports = TemplateMapsController; @@ -430,6 +433,9 @@ TemplateMapsController.prototype.instantiateTemplate = function(req, res, templa } var tplhash = self.templateMaps.fingerPrint(template).substring(0,8); layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid; + + self.surrogateKeysCache.tag(res, new self.NamedMapsCacheEntry(cdbuser, template.name)); + return layergroup; }, callback