From c77dce105ce2d94afead05cbeb8e1cc48595184b Mon Sep 17 00:00:00 2001 From: Luis Bosque Date: Mon, 4 Mar 2013 11:37:10 +0100 Subject: [PATCH 01/22] Target v1.1.9 --- NEWS.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 57db4db3..c809cc4b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +1.1.9 +----- + 1.1.8 ----- * Require Windshaft-0.9.1, to reduce harmfulness of cache_buster param diff --git a/package.json b/package.json index 7400f21b..d28ef1fb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.1.7", + "version": "1.1.9", "description": "A map tile server for CartoDB", "url": "https://github.com/Vizzuality/Windshaft-cartodb", "licenses": [{ From 49aad435b9227a91f7b249a3afea6fe19fe3981d Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 08:48:30 +0100 Subject: [PATCH 02/22] More ignores --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 96b65b68..55d81916 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -node_modules +node_modules* config/environments/*.js .idea tools/munin/windshaft.conf +logs/ +pids/ From e8cbc666e232f3acf321a92b1c0da77105dcc9d1 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 10:36:28 +0100 Subject: [PATCH 03/22] Handle SQL API errors by logging them and requesting NO cache SQL api is used to determine the list of source tables affected by a query. Before this commit, the X-Cache-Channel header set on sql api error was an arbitrary 'table' string, now the header is omitted, the error logged and Cache-Control and Pragma headers are sent as an attempt to request no caching. The code includes test for this mechanism. --- config/environments/test.js.example | 6 ++- lib/cartodb/cache_validator.js | 25 +++++---- lib/cartodb/server_options.js | 30 +++++++---- test/acceptance/server.js | 82 ++++++++++++++++++++++++++++- test/support/sql/gadm4.sql | 4 +- 5 files changed, 121 insertions(+), 26 deletions(-) diff --git a/config/environments/test.js.example b/config/environments/test.js.example index b97b9a6d..ff636581 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -48,8 +48,10 @@ var config = { } ,sqlapi: { protocol: 'http', - host: 'localhost.lan', - port: 8080, + host: '', + // This port will be used by "make check" for testing purposes + // It must be available + port: 1080, version: 'v1' } ,varnish: { diff --git a/lib/cartodb/cache_validator.js b/lib/cartodb/cache_validator.js index 479568f6..aa551df6 100644 --- a/lib/cartodb/cache_validator.js +++ b/lib/cartodb/cache_validator.js @@ -36,7 +36,7 @@ function generateCacheChannel(req, callback){ // use cache if present if (!_.isNull(channelCache[sql_md5]) && !_.isUndefined(channelCache[sql_md5])) { - callback(channelCache[sql_md5]); + callback(null, channelCache[sql_md5]); } else{ // strip out windshaft/mapnik inserted sql if present var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/); @@ -54,21 +54,28 @@ function generateCacheChannel(req, callback){ } // call sql api - request.get({url:sqlapi, qs:qs, json:true}, function(err, response, body){ - if (!err && response.statusCode == 200) { - tableNames = body.rows[0].cdb_querytables.split(/^\{(.*)\}$/)[1]; - } else { - //oops, no SQL API. Just cache using fallback 'table' key - tableNames = 'table'; + request.get({url:sqlapi, qs:qs, json:true}, function(err, res, body){ + var epref = 'could not detect source tables using SQL api at ' + sqlapi; + if (err){ + var msg = err.message ? err.message : err; + callback(new Error(epref + ': ' + msg)); + return; } + if (res.statusCode != 200) { + var msg = res.body.error ? res.body.error : res.body; + callback(new Error(epref + ': ' + msg)); + return; + } + var qtables = body.rows[0].cdb_querytables; + tableNames = qtables.split(/^\{(.*)\}$/)[1]; cacheChannel = buildCacheChannel(dbName,tableNames); channelCache[sql_md5] = cacheChannel; // store for caching - callback(cacheChannel); + callback(null, cacheChannel); }); } } else { cacheChannel = buildCacheChannel(dbName,tableNames); - callback(cacheChannel); + callback(null, cacheChannel); } } diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index ac9c458f..ac1bc3bf 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -57,17 +57,27 @@ module.exports = function(){ // skip non-GET requests, or requests for which there's no response if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; } var res = req.res; - var ttl = global.environment.varnish.ttl || 86400; - Cache.generateCacheChannel(req, function(channel){ - res.header('X-Cache-Channel', channel); - var cache_policy = req.query.cache_policy; - if ( cache_policy == 'persist' ) { - res.header('Cache-Control', 'public,max-age=31536000'); // 1 year + var cache_policy = req.query.cache_policy; + if ( cache_policy == 'persist' ) { + res.header('Cache-Control', 'public,max-age=31536000'); // 1 year + } else { + var ttl = global.environment.varnish.ttl || 86400; + res.header('Last-Modified', new Date().toUTCString()); + res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public'); + } + Cache.generateCacheChannel(req, function(err, channel){ + if ( ! err ) { + res.header('X-Cache-Channel', channel); + cb(null, channel); } else { - res.header('Last-Modified', new Date().toUTCString()); - res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public'); + // avoid caching this result + // (temptative, what Varnish does is out of our control) + res.header('Cache-Control', 'no-cache,no-store,max-age=0,must-revalidate'); + res.header('Pragma', 'no-cache'); + console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err )); + // TODO: evaluate if we should bubble up the error instead + cb(null, 'ERROR'); } - cb(null, channel); // add last-modified too ? }); } @@ -127,7 +137,7 @@ module.exports = function(){ if (!_.isNull(data)) _.extend(req.params, {geom_type: data}); - that.addCacheChannel(req, function(err, chan) { + that.addCacheChannel(req, function(err) { callback(err, req); }); } diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 3e419015..262d7663 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -6,6 +6,8 @@ var querystring = require('querystring'); var semver = require('semver'); var mapnik = require('mapnik'); var Step = require('step'); +var http = require('http'); +var url = require('url'); require(__dirname + '/../support/test_helper'); @@ -17,6 +19,7 @@ server.setMaxListeners(0); suite('server', function() { var redis_client = redis.createClient(global.environment.redis.port); + var sqlapi_server; var default_style = semver.satisfies(mapnik.versions.mapnik, '<2.1.0') ? @@ -30,7 +33,19 @@ suite('server', function() { var test_style_black_200 = "#test_table{marker-fill:black;marker-line-color:red;marker-width:10}"; var test_style_black_210 = "#test_table{marker-fill:black;marker-line-color:red;marker-width:20}"; - suiteSetup(function(){ + suiteSetup(function(done){ + sqlapi_server = http.createServer(function(req,res) { + var query = url.parse(req.url, true).query; + if ( query.q.match('SQLAPIERROR') ) { + res.statusCode = 400; + res.write(JSON.stringify({'error':'Some error occurred'})); + } else { + res.write(JSON.stringify({rows: [ { 'cdb_querytables': '{' + + JSON.stringify(query) + '}' } ]})); + } + res.end(); + }); + sqlapi_server.listen(global.environment.sqlapi.port, done); }); ///////////////////////////////////////////////////////////////////////////////// @@ -916,6 +931,69 @@ suite('server', function() { ); }); + test("uses sqlapi to figure source data of query", function(done){ + var qo = { + sql: "SELECT g.cartodb_id, g.codineprov, t.the_geom_webmercator " + + "FROM gadm4 g, test_table t " + + "WHERE g.cartodb_id = t.cartodb_id", + map_key: 1234 + }; + var sqlapi; + Step( + function sendRequest(err) { + assert.response(server, { + headers: {host: 'localhost'}, + url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo), + method: 'GET' + },{}, this); + }, + function checkResponse(res) { + assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); + var ct = res.headers['content-type']; + assert.equal(ct, 'image/png'); + var cc = res.headers['x-cache-channel']; + var dbname = 'cartodb_test_user_1_db' + assert.equal(cc.substring(0, dbname.length), dbname); + var jsonquery = cc.substring(dbname.length+1); + var sentquery = JSON.parse(jsonquery); + assert.equal(sentquery.api_key, qo.map_key); + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + qo.sql + '$windshaft$)'); + done(); + } + ); + }); + + test("requests to skip cache on sqlapi error", function(done){ + var qo = { + sql: "SELECT g.cartodb_id, g.codineprov, t.the_geom_webmercator " + + ", 'SQLAPIERROR' is not null " + + "FROM gadm4 g, test_table t " + + "WHERE g.cartodb_id = t.cartodb_id", + map_key: 1234 + }; + var sqlapi; + Step( + function sendRequest(err) { + assert.response(server, { + headers: {host: 'localhost'}, + url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo), + method: 'GET' + },{}, this); + }, + function checkResponse(res) { + assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); + var ct = res.headers['content-type']; + assert.equal(ct, 'image/png'); + // does NOT send an x-cache-channel + assert.ok(!res.headers.hasOwnProperty('x-cache-channel')); + // attempts to tell varnish NOT to cache + assert.equal(res.headers['cache-control'], 'no-cache,no-store,max-age=0,must-revalidate'); + assert.equal(res.headers['pragma'], 'no-cache'); + done(); + } + ); + }); + ///////////////////////////////////////////////////////////////////////////////// // // DELETE CACHE @@ -1040,7 +1118,7 @@ suite('server', function() { // 'map_style|null|publicuser|my_table', redis_client.keys("map_style|*", function(err, matches) { _.each(matches, function(k) { redis_client.del(k); }); - done(); + sqlapi_server.close(done); }); }); diff --git a/test/support/sql/gadm4.sql b/test/support/sql/gadm4.sql index 4f257192..525053ae 100644 --- a/test/support/sql/gadm4.sql +++ b/test/support/sql/gadm4.sql @@ -97,8 +97,6 @@ ALTER TABLE ONLY gadm4 CREATE INDEX bdll25_provincias_4326_2_the_geom_webmercator_idx ON gadm4 USING gist (the_geom_webmercator); --- development_cartodb_user_3 role -CREATE USER development_cartodb_user_3; -GRANT ALL ON TABLE gadm4 TO development_cartodb_user_3; +GRANT ALL ON TABLE gadm4 TO test_cartodb_user_1; GRANT SELECT ON TABLE gadm4 TO publicuser; From 4622dac81bc5908554074085fc3810196cb2ad6f Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 10:40:06 +0100 Subject: [PATCH 04/22] Update NEWS --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index c809cc4b..44e6e96e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ 1.1.9 ----- +* Handle SQL API errors by requesting no cache 1.1.8 ----- From fcd6d55ba4805cabedcd00fad397534c4e4f3230 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 11:55:57 +0100 Subject: [PATCH 05/22] Draft test for X-Cache-Channel in multilayer response The test is disabled because it fails --- test/acceptance/multilayer.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 91e37b34..ab884dc6 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -6,6 +6,8 @@ var querystring = require('querystring'); var semver = require('semver'); var mapnik = require('mapnik'); var Step = require('step'); +var http = require('http'); +var url = require('url'); require(__dirname + '/../support/test_helper'); @@ -19,8 +21,21 @@ server.setMaxListeners(0); suite('multilayer', function() { var redis_client = redis.createClient(global.environment.redis.port); + var sqlapi_server; - suiteSetup(function(){ + suiteSetup(function(done){ + sqlapi_server = http.createServer(function(req,res) { + var query = url.parse(req.url, true).query; + if ( query.q.match('SQLAPIERROR') ) { + res.statusCode = 400; + res.write(JSON.stringify({'error':'Some error occurred'})); + } else { + res.write(JSON.stringify({rows: [ { 'cdb_querytables': '{' + + JSON.stringify(query) + '}' } ]})); + } + res.end(); + }); + sqlapi_server.listen(global.environment.sqlapi.port, done); }); test("layergroup with 2 layers, each with its style", function(done) { @@ -74,6 +89,22 @@ suite('multilayer', function() { }, {}, function(res) { assert.equal(res.statusCode, 200, res.body); assert.equal(res.headers['content-type'], "image/png"); + + // Check X-Cache-Channel + var cc = res.headers['x-cache-channel']; + assert.ok(cc); + var dbname = 'cartodb_test_user_1_db' + assert.equal(cc.substring(0, dbname.length), dbname); + var jsonquery = cc.substring(dbname.length+1); +//console.log('jsonquery: '+ jsonquery); + // FIXME: this is currently _undefined_ ! + +/* + var sentquery = JSON.parse(jsonquery); + assert.equal(sentquery.api_key, qo.map_key); + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + qo.sql + '$windshaft$)'); +*/ + assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2, function(err, similarity) { next(err); @@ -140,7 +171,7 @@ suite('multilayer', function() { // 'map_style|null|publicuser|my_table', redis_client.keys("map_style|*", function(err, matches) { _.each(matches, function(k) { redis_client.del(k); }); - done(); + sqlapi_server.close(done); }); }); From 402fc90e630871f45908fd2979307d2acf6f5e7f Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 12:01:35 +0100 Subject: [PATCH 06/22] Absence of X-Cache-Channel will be enough for Varnish to skip caching Do not override Cache-Control in this case, which means let the clients or geographical proxies cache the response with usual TTL. --- NEWS.md | 2 +- lib/cartodb/server_options.js | 4 ---- test/acceptance/server.js | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index 44e6e96e..51b83821 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ 1.1.9 ----- -* Handle SQL API errors by requesting no cache +* Handle SQL API errors by requesting no Varnish cache 1.1.8 ----- diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index ac1bc3bf..ef847306 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -70,10 +70,6 @@ module.exports = function(){ res.header('X-Cache-Channel', channel); cb(null, channel); } else { - // avoid caching this result - // (temptative, what Varnish does is out of our control) - res.header('Cache-Control', 'no-cache,no-store,max-age=0,must-revalidate'); - res.header('Pragma', 'no-cache'); console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err )); // TODO: evaluate if we should bubble up the error instead cb(null, 'ERROR'); diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 262d7663..4250c7de 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -986,9 +986,6 @@ suite('server', function() { assert.equal(ct, 'image/png'); // does NOT send an x-cache-channel assert.ok(!res.headers.hasOwnProperty('x-cache-channel')); - // attempts to tell varnish NOT to cache - assert.equal(res.headers['cache-control'], 'no-cache,no-store,max-age=0,must-revalidate'); - assert.equal(res.headers['pragma'], 'no-cache'); done(); } ); From 899d8a3c64e7b8cbcdc1e88609cb28e324499879 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 16:40:54 +0100 Subject: [PATCH 07/22] Run multilayer test as part of "make check" --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c6eee208..6c479c25 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ check-local: config/environments/test.js test/unit/cartodb/redis_pool.test.js \ test/unit/cartodb/req2params.test.js \ test/acceptance/cache_validator.js \ - test/acceptance/server.js + test/acceptance/server.js \ + test/acceptance/multilayer.js check-submodules: for sub in windshaft grainstore mapnik; do \ From dfc4a0239830346305b861e190593f8f9b63d55d Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 16:45:15 +0100 Subject: [PATCH 08/22] Fix X-Cache-Channel for multilayer (by token) responses Required upgrading Windshaft to 0.9.2 Includes testcases --- NEWS.md | 1 + lib/cartodb/cache_validator.js | 77 +----------------- lib/cartodb/server_options.js | 139 ++++++++++++++++++++++++++++++++- npm-shrinkwrap.json | 4 +- package.json | 2 +- test/acceptance/multilayer.js | 13 ++- 6 files changed, 147 insertions(+), 89 deletions(-) diff --git a/NEWS.md b/NEWS.md index 51b83821..14bd722c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ 1.1.9 ----- * Handle SQL API errors by requesting no Varnish cache +* Fix X-Cache-Channel for multilayer (by token) responses 1.1.8 ----- diff --git a/lib/cartodb/cache_validator.js b/lib/cartodb/cache_validator.js index aa551df6..8449861e 100644 --- a/lib/cartodb/cache_validator.js +++ b/lib/cartodb/cache_validator.js @@ -1,8 +1,5 @@ var _ = require('underscore'), Varnish = require('node-varnish'), - request = require('request'), - crypto = require('crypto'), - channelCache = {}, varnish_queue = null; function init(host, port) { @@ -18,79 +15,7 @@ function invalidate_db(dbname, table) { } } -function generateCacheChannel(req, callback){ - var cacheChannel = ""; - - // use key to call sql api with sql request if present, else just return dbname and table name - // base key - var tableNames = req.params.table; - var dbName = req.params.dbname; - var username = req.headers.host.split('.')[0]; - - // replace tableNames with the results of the explain if present - if (_.isString(req.params.sql) && req.params.sql != ''){ - // initialise MD5 key of sql for cache lookups - var sql_md5 = generateMD5(req.params.sql); - var api = global.environment.sqlapi; - var qs = {}; - - // use cache if present - if (!_.isNull(channelCache[sql_md5]) && !_.isUndefined(channelCache[sql_md5])) { - callback(null, channelCache[sql_md5]); - } else{ - // strip out windshaft/mapnik inserted sql if present - var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/); - sql = (sql != null) ? sql[1] : req.params.sql; - - // build up api string - var sqlapi = api.protocol + '://' + username + '.' + api.host + ':' + api.port + '/api/' + api.version + '/sql' - - // add query to querystring - qs.q = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; - - // add api_key if present in tile request (means table is private) - if (_.isString(req.params.map_key) && req.params.map_key != ''){ - qs.api_key = req.params.map_key; - } - - // call sql api - request.get({url:sqlapi, qs:qs, json:true}, function(err, res, body){ - var epref = 'could not detect source tables using SQL api at ' + sqlapi; - if (err){ - var msg = err.message ? err.message : err; - callback(new Error(epref + ': ' + msg)); - return; - } - if (res.statusCode != 200) { - var msg = res.body.error ? res.body.error : res.body; - callback(new Error(epref + ': ' + msg)); - return; - } - var qtables = body.rows[0].cdb_querytables; - tableNames = qtables.split(/^\{(.*)\}$/)[1]; - cacheChannel = buildCacheChannel(dbName,tableNames); - channelCache[sql_md5] = cacheChannel; // store for caching - callback(null, cacheChannel); - }); - } - } else { - cacheChannel = buildCacheChannel(dbName,tableNames); - callback(null, cacheChannel); - } -} - -function buildCacheChannel(dbName, tableNames){ - return dbName + ':' + tableNames; -} - -function generateMD5(data){ - var hash = crypto.createHash('md5'); - hash.update(data); - return hash.digest('hex'); -} - module.exports = { init: init, - invalidate_db: invalidate_db, - generateCacheChannel: generateCacheChannel + invalidate_db: invalidate_db } diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index ef847306..328b2e87 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -3,6 +3,8 @@ var _ = require('underscore') , cartoData = require('./carto_data') , Cache = require('./cache_validator') , mapnik = require('mapnik') + , crypto = require('crypto') + , request = require('request') ; module.exports = function(){ @@ -44,6 +46,111 @@ module.exports = function(){ + me.grainstore.mapnik_version + ")"); } +/* This whole block is about generating X-Cache-Channel { */ + + // TODO: review lifetime of elements of this cache + // NOTE: by-token indices should only be dropped when + // the corresponding layegroup is dropped, because + // we have no SQL after layer creation. + me.channelCache = {}; + + me.affectedTables = function (username, api_key, sql, callback) { + + var api = global.environment.sqlapi; + + // build up api string + var sqlapi = api.protocol + '://' + username + '.' + api.host + ':' + api.port + '/api/' + api.version + '/sql' + + var qs = {}; + + // add query to querystring + qs.q = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; + + // add api_key if given + if (_.isString(api_key) && api_key != ''){ qs.api_key = api_key; } + + // call sql api + request.get({url:sqlapi, qs:qs, json:true}, function(err, res, body){ + var epref = 'could not detect source tables using SQL api at ' + sqlapi; + if (err){ + var msg = err.message ? err.message : err; + callback(new Error(epref + ': ' + msg)); + return; + } + if (res.statusCode != 200) { + var msg = res.body.error ? res.body.error : res.body; + callback(new Error(epref + ': ' + msg)); + return; + } + var qtables = body.rows[0].cdb_querytables; + var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + callback(null, tableNames); + }); + }, + + me.buildCacheChannel = function (dbName, tableNames){ + return dbName + ':' + tableNames; + }; + + me.generateMD5 = function(data){ + var hash = crypto.createHash('md5'); + hash.update(data); + return hash.digest('hex'); + } + + me.generateCacheChannel = function(req, callback){ + + // use key to call sql api with sql request if present, else + // just return dbname and table name base key + var dbName = req.params.dbname; + + var cacheKey = [ dbName ]; + if ( req.params.token ) cacheKey.push(req.params.token); + else if ( req.params.sql ) cacheKey.push( me.generateMD5(req.params.sql) ); + cacheKey = cacheKey.join(':'); + + if ( me.channelCache.hasOwnProperty(cacheKey) ) { + callback(null, me.channelCache[cacheKey]); + return; + } + + if ( req.params.token ) { + if ( ! me.channelCache.hasOwnProperty(cacheKey) ) { + callback(new Error('missing channel cache for token ' + req.params.token)); + } else { + callback(null, me.channelCache[cacheKey]); + } + return; + } + + if ( ! req.params.sql && ! req.params.token ) { + var cacheChannel = me.buildCacheChannel(dbName, req.params.table); + // not worth caching this + callback(null, cacheChannel); + return; + } + + if ( ! req.params.sql ) { +console.log('req:'); console.dir(req); + callback(new Error("this request doesn't need an X-Cache-Channel generated")); + return; + } + + var dbName = req.params.dbname; + var username = req.headers.host.split('.')[0]; + + // strip out windshaft/mapnik inserted sql if present + var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/); + sql = (sql != null) ? sql[1] : req.params.sql; + + me.affectedTables(username, req.params.map_key, sql, function(err, tableNames) { + if ( err ) { callback(err); return; } + var cacheChannel = me.buildCacheChannel(dbName,tableNames); + me.channelCache[cacheKey] = cacheChannel; // store for caching + callback(null, cacheChannel); + }); + }; + // Set the cache chanel info to invalidate the cache on the frontend server // // @param req The request object. @@ -65,7 +172,8 @@ module.exports = function(){ res.header('Last-Modified', new Date().toUTCString()); res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public'); } - Cache.generateCacheChannel(req, function(err, channel){ + + me.generateCacheChannel(req, function(err, channel){ if ( ! err ) { res.header('X-Cache-Channel', channel); cb(null, channel); @@ -75,7 +183,34 @@ module.exports = function(){ cb(null, 'ERROR'); } }); - } + }; + + me.afterLayergroupCreate = function(req, response, callback) { + var token = response.layergroupid; + var mapconfig = req.body; + + var sql = []; + _.each(mapconfig.layers, function(lyr) { + sql.push(lyr.options.sql); + }); + sql = sql.join(';'); + console.log('afterLayergroupCreate: sql:'+sql); + + var dbName = req.params.dbname; + var usr = req.headers.host.split('.')[0]; + var key = req.params.map_key; + + var cacheKey = dbName + ':' + token; + + me.affectedTables(usr, key, sql, function(err, tableNames) { + if ( err ) { callback(err); return; } + var cacheChannel = me.buildCacheChannel(dbName,tableNames); + me.channelCache[cacheKey] = cacheChannel; // store for caching + callback(null); + }); + }; + +/* X-Cache-Channel generation } */ /** * Whitelist input and get database name & default geometry type from diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3ef03a44..b61b56fe 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.1.7", + "version": "1.1.9", "dependencies": { "cluster2": { "version": "0.3.5-cdb02", @@ -257,7 +257,7 @@ } }, "windshaft": { - "version": "0.9.1", + "version": "0.9.2", "dependencies": { "express": { "version": "2.5.11", diff --git a/package.json b/package.json index d28ef1fb..cad96acd 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "node-varnish": "0.1.1", "underscore" : "~1.3.3", "grainstore" : "~0.11.1", - "windshaft" : "~0.9.1", + "windshaft" : "~0.9.2", "step": "0.0.x", "generic-pool": "~1.0.12", "redis": "0.7.2", diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index ab884dc6..2b1a27dd 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -92,18 +92,15 @@ suite('multilayer', function() { // Check X-Cache-Channel var cc = res.headers['x-cache-channel']; - assert.ok(cc); + assert.ok(cc); var dbname = 'cartodb_test_user_1_db' assert.equal(cc.substring(0, dbname.length), dbname); var jsonquery = cc.substring(dbname.length+1); -//console.log('jsonquery: '+ jsonquery); - // FIXME: this is currently _undefined_ ! - -/* var sentquery = JSON.parse(jsonquery); - assert.equal(sentquery.api_key, qo.map_key); - assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + qo.sql + '$windshaft$)'); -*/ + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + ';' + + layergroup.layers[1].options.sql + + '$windshaft$)'); assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2, function(err, similarity) { From 4605bd1e1d3ade37772c4209cba6080e38c7f13a Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 13 Mar 2013 18:41:37 +0100 Subject: [PATCH 09/22] Add last_modified field to POST layergroup response (#72) Includes testcases --- NEWS.md | 1 + lib/cartodb/server_options.js | 68 +++++++++++++++++++++++++---------- test/acceptance/multilayer.js | 36 ++++++++++--------- test/acceptance/server.js | 15 ++------ test/support/SQLAPIEmu.js | 29 +++++++++++++++ 5 files changed, 101 insertions(+), 48 deletions(-) create mode 100644 test/support/SQLAPIEmu.js diff --git a/NEWS.md b/NEWS.md index 14bd722c..e496be88 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ ----- * Handle SQL API errors by requesting no Varnish cache * Fix X-Cache-Channel for multilayer (by token) responses +* Add last_modified field to POST layergroup response (#72) 1.1.8 ----- diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 328b2e87..aaa58fea 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -54,42 +54,70 @@ module.exports = function(){ // we have no SQL after layer creation. me.channelCache = {}; - me.affectedTables = function (username, api_key, sql, callback) { - + // Run a query through the SQL api + me.sqlQuery = function (username, api_key, sql, callback) { var api = global.environment.sqlapi; // build up api string var sqlapi = api.protocol + '://' + username + '.' + api.host + ':' + api.port + '/api/' + api.version + '/sql' - var qs = {}; - - // add query to querystring - qs.q = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; + var qs = { q: sql } // add api_key if given - if (_.isString(api_key) && api_key != ''){ qs.api_key = api_key; } + if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; } // call sql api request.get({url:sqlapi, qs:qs, json:true}, function(err, res, body){ - var epref = 'could not detect source tables using SQL api at ' + sqlapi; if (err){ - var msg = err.message ? err.message : err; - callback(new Error(epref + ': ' + msg)); + callback(err); return; } if (res.statusCode != 200) { var msg = res.body.error ? res.body.error : res.body; - callback(new Error(epref + ': ' + msg)); + callback(new Error('unexpected response status (' + res.statusCode + ') for sql query: ' + sql)); return; } - var qtables = body.rows[0].cdb_querytables; + callback(null, body.rows); + }); + }; + + me.findLastUpdated = function (username, api_key, tableNames, callback) { + + var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) FROM CDB_TableMetadata WHERE m.tabname::name = any ({' + + tableNames.join(',') + '})'; + + // call sql api + me.sqlQuery(username, api_key, sql, function(err, rows){ + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not find last updated timestamp: ' + msg)); + return; + } + var last_updated = rows[0].max; + callback(null, last_updated); + }); + }; + + me.affectedTables = function (username, api_key, sql, callback) { + + var sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)'; + + // call sql api + me.sqlQuery(username, api_key, sql, function(err, rows){ + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not fetch source tables: ' + msg)); + return; + } + var qtables = rows[0].cdb_querytables; var tableNames = qtables.split(/^\{(.*)\}$/)[1]; + tableNames = tableNames.split(','); callback(null, tableNames); }); - }, + }; me.buildCacheChannel = function (dbName, tableNames){ - return dbName + ':' + tableNames; + return dbName + ':' + tableNames.join(','); }; me.generateMD5 = function(data){ @@ -124,14 +152,13 @@ module.exports = function(){ } if ( ! req.params.sql && ! req.params.token ) { - var cacheChannel = me.buildCacheChannel(dbName, req.params.table); + var cacheChannel = me.buildCacheChannel(dbName, [req.params.table]); // not worth caching this callback(null, cacheChannel); return; } if ( ! req.params.sql ) { -console.log('req:'); console.dir(req); callback(new Error("this request doesn't need an X-Cache-Channel generated")); return; } @@ -194,7 +221,6 @@ console.log('req:'); console.dir(req); sql.push(lyr.options.sql); }); sql = sql.join(';'); - console.log('afterLayergroupCreate: sql:'+sql); var dbName = req.params.dbname; var usr = req.headers.host.split('.')[0]; @@ -203,10 +229,16 @@ console.log('req:'); console.dir(req); var cacheKey = dbName + ':' + token; me.affectedTables(usr, key, sql, function(err, tableNames) { + if ( err ) { callback(err); return; } var cacheChannel = me.buildCacheChannel(dbName,tableNames); me.channelCache[cacheKey] = cacheChannel; // store for caching - callback(null); + // find last updated + me.findLastUpdated(usr, key, tableNames, function(err, lastUpdated) { + if ( err ) { callback(err); return; } + response.last_updated = lastUpdated; + callback(null); + }); }); }; diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 2b1a27dd..601983df 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -6,8 +6,7 @@ var querystring = require('querystring'); var semver = require('semver'); var mapnik = require('mapnik'); var Step = require('step'); -var http = require('http'); -var url = require('url'); +var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js'); require(__dirname + '/../support/test_helper'); @@ -24,18 +23,7 @@ suite('multilayer', function() { var sqlapi_server; suiteSetup(function(done){ - sqlapi_server = http.createServer(function(req,res) { - var query = url.parse(req.url, true).query; - if ( query.q.match('SQLAPIERROR') ) { - res.statusCode = 400; - res.write(JSON.stringify({'error':'Some error occurred'})); - } else { - res.write(JSON.stringify({rows: [ { 'cdb_querytables': '{' + - JSON.stringify(query) + '}' } ]})); - } - res.end(); - }); - sqlapi_server.listen(global.environment.sqlapi.port, done); + sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); }); test("layergroup with 2 layers, each with its style", function(done) { @@ -70,9 +58,23 @@ suite('multilayer', function() { assert.equal(res.statusCode, 200, res.body); var parsedBody = JSON.parse(res.body); var expectedBody = { layergroupid: expected_token }; - // TODO: check last modified - //expectedBody.layercount = 2; - if ( expected_token ) assert.deepEqual(parsedBody, expectedBody); + // check last modified + var qTables = JSON.stringify({ + 'q': 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + ';' + + layergroup.layers[1].options.sql + + '$windshaft$)' + }); + expectedBody.last_updated = JSON.stringify({ + 'q': 'SELECT EXTRACT(EPOCH FROM max(updated_at)) ' + + 'FROM CDB_TableMetadata WHERE m.tabname::name = any ({' + + qTables + '})' + }); + if ( expected_token ) { + //assert.equal(parsedBody.layergroupid, expectedBody.layergroupid); + //assert.equal(parsedBody.last_updated, expectedBody.last_updated); + assert.deepEqual(parsedBody, expectedBody); + } else expected_token = parsedBody.layergroupid; next(null, res); }); diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 4250c7de..33f8cc30 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -7,7 +7,7 @@ var semver = require('semver'); var mapnik = require('mapnik'); var Step = require('step'); var http = require('http'); -var url = require('url'); +var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js'); require(__dirname + '/../support/test_helper'); @@ -34,18 +34,7 @@ suite('server', function() { var test_style_black_210 = "#test_table{marker-fill:black;marker-line-color:red;marker-width:20}"; suiteSetup(function(done){ - sqlapi_server = http.createServer(function(req,res) { - var query = url.parse(req.url, true).query; - if ( query.q.match('SQLAPIERROR') ) { - res.statusCode = 400; - res.write(JSON.stringify({'error':'Some error occurred'})); - } else { - res.write(JSON.stringify({rows: [ { 'cdb_querytables': '{' + - JSON.stringify(query) + '}' } ]})); - } - res.end(); - }); - sqlapi_server.listen(global.environment.sqlapi.port, done); + sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); }); ///////////////////////////////////////////////////////////////////////////////// diff --git a/test/support/SQLAPIEmu.js b/test/support/SQLAPIEmu.js new file mode 100644 index 00000000..68ab26db --- /dev/null +++ b/test/support/SQLAPIEmu.js @@ -0,0 +1,29 @@ +var http = require('http'); +var url = require('url'); + +var o = function(port, cb) { + + this.sqlapi_server = http.createServer(function(req,res) { + var query = url.parse(req.url, true).query; + if ( query.q.match('SQLAPIERROR') ) { + res.statusCode = 400; + res.write(JSON.stringify({'error':'Some error occurred'})); + } else { + var qs = JSON.stringify(query); + var row = { + // This is the structure of the known query sent by tiler + 'cdb_querytables': '{' + qs + '}', + 'max': qs + }; + res.write(JSON.stringify({rows: [ row ]})); + } + res.end(); + }).listen(port, cb); +}; + +o.prototype.close = function(cb) { + this.sqlapi_server.close(cb); +}; + +module.exports = o; + From 3ff3dc2c97a51350e1ae2fa077352d2b3c484e98 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 15 Mar 2013 19:25:13 +0100 Subject: [PATCH 10/22] Cleanup, handle error in req2param on flushCache --- lib/cartodb/cartodb_windshaft.js | 4 ++-- lib/cartodb/server_options.js | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index ded52079..b20212e4 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -82,10 +82,10 @@ var CartodbWindshaft = function(serverOptions) { ws.del(serverOptions.base_url + '/flush_cache', function(req, res){ ws.doCORS(res); Step( - function(){ + function flushCache(){ serverOptions.flushCache(req, serverOptions.cache_enabled ? Cache : null, this); }, - function(err, data){ + function sendResponse(err, data){ if (err){ ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE'); //res.send(500); diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index aaa58fea..3be61a58 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -354,11 +354,17 @@ module.exports = function(){ var that = this; Step( - function(){ + function getParams(){ + // this is mostly to compute req.params.dbname that.req2params(req, this); }, - function(err, data){ - if (err) throw err; + function flushInternalCache(err){ + // TODO: implement this, see + // http://github.com/Vizzuality/Windshaft-cartodb/issues/73 + return true; + }, + function flushVarnishCache(err){ + if (err) { callback(err); return; } if(Cache) { Cache.invalidate_db(req.params.dbname, req.params.table); } From d95ac85b172ccfd7c187e08e57615f7d890483ae Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Mon, 18 Mar 2013 16:37:31 +0100 Subject: [PATCH 11/22] Deprecate USR1 signal handling, add USR2 with same semantic Closes #71 --- NEWS.md | 1 + app.js | 5 +++++ tools/munin/windshaft | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index e496be88..3e39115d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ * Handle SQL API errors by requesting no Varnish cache * Fix X-Cache-Channel for multilayer (by token) responses * Add last_modified field to POST layergroup response (#72) +* Deprecate signal handler for USR1, add handler for USR2 (#71) 1.1.8 ----- diff --git a/app.js b/app.js index 845a3cd6..7b5af0b5 100755 --- a/app.js +++ b/app.js @@ -46,6 +46,11 @@ ws.on('listening', function() { console.log("Windshaft tileserver started on " + global.environment.host + ':' + global.environment.port); }); +// DEPRECATED, use SIGUSR2 process.on('SIGUSR1', function() { ws.dumpCacheStats(); }); + +process.on('SIGUSR2', function() { + ws.dumpCacheStats(); +}); diff --git a/tools/munin/windshaft b/tools/munin/windshaft index 77adcb23..30adaf15 100755 --- a/tools/munin/windshaft +++ b/tools/munin/windshaft @@ -67,7 +67,7 @@ for pid in ${pids}; do log=$(grep "${pid}" "${tmpreport}" | grep -w 1w | awk '{print $9}') if test -e "${log}"; then - kill -USR1 "${pid}" + kill -USR2 "${pid}" cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/^RenderCache/q' | wc -l) if test $cnt -gt $maxcache; then maxcache=$cnt; fi else From 4e7f92c60da3e4398fb35aa27e3604a84803bf40 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Mon, 18 Mar 2013 18:14:18 +0100 Subject: [PATCH 12/22] Write a deprecation warning when handling SIGUSR1 (#71) --- app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app.js b/app.js index 7b5af0b5..ab75651a 100755 --- a/app.js +++ b/app.js @@ -48,6 +48,7 @@ ws.on('listening', function() { // DEPRECATED, use SIGUSR2 process.on('SIGUSR1', function() { + console.log('WARNING: handling of SIGUSR1 by Windshaft-CartoDB is deprecated, please send SIGUSR2 instead'); ws.dumpCacheStats(); }); From e53122de7e7d86ecc5f19190fa878df66cc9c323 Mon Sep 17 00:00:00 2001 From: javi Date: Thu, 21 Mar 2013 11:39:55 +0100 Subject: [PATCH 13/22] fixed last_update in laytergroup response --- lib/cartodb/server_options.js | 11 +++++++---- test/acceptance/multilayer.js | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 3be61a58..24c0b4df 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -82,9 +82,8 @@ module.exports = function(){ }; me.findLastUpdated = function (username, api_key, tableNames, callback) { - - var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) FROM CDB_TableMetadata WHERE m.tabname::name = any ({' - + tableNames.join(',') + '})'; + var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{' + + tableNames.join(',') + '}\')'; // call sql api me.sqlQuery(username, api_key, sql, function(err, rows){ @@ -93,7 +92,11 @@ module.exports = function(){ callback(new Error('could not find last updated timestamp: ' + msg)); return; } - var last_updated = rows[0].max; + // when the table has not updated_at means it hasn't been changed so a default last_updated is set + var last_updated = 0; + if(rows.length !== 0) { + last_updated = rows[0].max || 0; + } callback(null, last_updated); }); }; diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 601983df..9691fff0 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -66,9 +66,9 @@ suite('multilayer', function() { + '$windshaft$)' }); expectedBody.last_updated = JSON.stringify({ - 'q': 'SELECT EXTRACT(EPOCH FROM max(updated_at)) ' - + 'FROM CDB_TableMetadata WHERE m.tabname::name = any ({' - + qTables + '})' + 'q': 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max ' + + 'FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{' + + qTables + '}\')' }); if ( expected_token ) { //assert.equal(parsedBody.layergroupid, expectedBody.layergroupid); From 2da8c4491411a623822589ff3984d858f25ba882 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 22 Mar 2013 12:42:13 +0100 Subject: [PATCH 14/22] Fix support for ampersend characters in CartoCSS Required upgrading grainstore dependency to 0.12.2 --- NEWS.md | 1 + npm-shrinkwrap.json | 13 ++++++++----- package.json | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index 3e39115d..e1fd64fb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ * Fix X-Cache-Channel for multilayer (by token) responses * Add last_modified field to POST layergroup response (#72) * Deprecate signal handler for USR1, add handler for USR2 (#71) +* Fix support for ampersend characters in CartoCSS 1.1.8 ----- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b61b56fe..02733829 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -187,11 +187,11 @@ "version": "1.3.3" }, "grainstore": { - "version": "0.11.1", + "version": "0.11.2", "dependencies": { "carto": { - "version": "0.9.3-cdb2", - "from": "git://github.com/CartoDB/carto.git#cdb-0.9", + "version": "0.9.3-cdb3", + "from": "git://github.com/CartoDB/carto.git#cdb-0.9.3-cdb3", "dependencies": { "mapnik-reference": { "version": "5.0.0-cdb1", @@ -213,8 +213,11 @@ "millstone": { "version": "0.5.15", "dependencies": { + "generic-pool": { + "version": "2.0.3" + }, "request": { - "version": "2.11.4", + "version": "2.12.0", "dependencies": { "form-data": { "version": "0.0.3", @@ -244,7 +247,7 @@ "version": "0.3.4" }, "sqlite3": { - "version": "2.1.5" + "version": "2.1.7" }, "mime": { "version": "1.2.9" diff --git a/package.json b/package.json index cad96acd..003cdf79 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "cluster2": "git://github.com/CartoDB/cluster2.git#cdb_production", "node-varnish": "0.1.1", "underscore" : "~1.3.3", - "grainstore" : "~0.11.1", + "grainstore" : "~0.11.2", "windshaft" : "~0.9.2", "step": "0.0.x", "generic-pool": "~1.0.12", From 001bf97d694a6d3c1af41ca9600f51111b2005c9 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 22 Mar 2013 18:55:59 +0100 Subject: [PATCH 15/22] Add support for LZMA compressed GET parameters You can now replace the whole query string with a single `lzma` parameter having as value an hex encoded LZMA compressed version of the whole query string as a JSON object. --- NEWS.md | 1 + lib/cartodb/server_options.js | 37 +++++++++++++++++++++++ npm-shrinkwrap.json | 3 ++ package.json | 3 +- test/acceptance/server.js | 57 +++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index e1fd64fb..c3d841c9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ * Add last_modified field to POST layergroup response (#72) * Deprecate signal handler for USR1, add handler for USR2 (#71) * Fix support for ampersend characters in CartoCSS +* Add support for LZMA compressed GET parameters 1.1.8 ----- diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 24c0b4df..abd60c6f 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -5,6 +5,7 @@ var _ = require('underscore') , mapnik = require('mapnik') , crypto = require('crypto') , request = require('request') + , LZMA = require('lzma/lzma_worker.js').LZMA ; module.exports = function(){ @@ -255,6 +256,42 @@ module.exports = function(){ */ me.req2params = function(req, callback){ + if ( req.query.lzma ) { + + // TODO: check ? + //console.log("type of req.query.lzma is " + typeof(req.query.lzma)); + //console.log("req.query.lzma is " + req.query.lzma); + + // Decode + var lzma = []; + for (var i=0; i 127 ) num = 127-num; + //console.log(i + " hex: " + hex + " decodes as " + num); + lzma.push( num ); + } + + // Decompress + //console.log("LZMA decompression starts with " + lzma); + LZMA.decompress( + lzma, + function(result) { + //console.log("LZMA decompression completed, payload: "); console.dir(result); + try { + req.query = JSON.parse(result); + me.req2params(req, callback); + } catch (err) { + callback(new Error('Error parsing lzma as JSON: ' + err)); + } + }, + function(percent) { // progress + //console.log("LZMA decompression " + percent + "%"); + } + ); + return; + } + // Whitelist query parameters and attach format var good_query = ['sql', 'geom_type', 'cache_buster', 'cache_policy', 'callback', 'interactivity', 'map_key', 'api_key', 'style', 'style_version', 'style_convert' ]; var bad_query = _.difference(_.keys(req.query), good_query); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 02733829..cb895d39 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -332,6 +332,9 @@ "mapnik": { "version": "0.7.22" }, + "lzma": { + "version": "1.2.2" + }, "semver": { "version": "1.1.3" }, diff --git a/package.json b/package.json index 003cdf79..71c6d619 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "redis": "0.7.2", "hiredis": "~0.1.14", "request": "2.9.202", - "mapnik": "~0.7.14" + "mapnik": "~0.7.14", + "lzma": "~1.2.2" }, "devDependencies": { "mocha": "1.2.1", diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 33f8cc30..cddc1cd4 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -7,6 +7,7 @@ var semver = require('semver'); var mapnik = require('mapnik'); var Step = require('step'); var http = require('http'); +var LZMA = require('lzma/lzma_worker.js').LZMA; var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js'); require(__dirname + '/../support/test_helper'); @@ -16,6 +17,27 @@ var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); var server = new CartodbWindshaft(serverOptions); server.setMaxListeners(0); +// Utility function to compress & encode LZMA +function lzma_compress_to_hex(payload, mode, callback) { + var HEX = [ '0','1','2','3','4','5','6','7', + '8','9','a','b','c','d','e','f' ]; + LZMA.compress(payload, mode, + function(ints) { + for (var i=0; i> 4; + var lo = ints[i] & 0x0f; + ints[i] = HEX[hi] + HEX[lo]; + }; + var hex = ints.join(''); + callback(null, hex); + }, + function(percent) { + //console.log("Compressing: " + percent + "%"); + } + ); +} + suite('server', function() { var redis_client = redis.createClient(global.environment.redis.port); @@ -782,6 +804,41 @@ suite('server', function() { }); }); + test("get'ing a tile with url specified 2.1.0 style (lzma version)", function(done){ + var qo = { + style: test_style_black_210, + style_version: '2.1.0', + cache_buster: 5 + }; + Step ( + function compressQuery () { + //console.log("Compressing starts"); + var next = this; + lzma_compress_to_hex(JSON.stringify(qo), 1, this); + //cosole.log("compress returned " + x ); + }, + function sendRequest(err, lzma) { + //console.log("Compressing ends: " + typeof(lzma) + " - " + lzma); + assert.response(server, { + headers: {host: 'localhost'}, + url: '/tiles/test_table/15/16046/12354.png?lzma=' + lzma, + method: 'GET', + encoding: 'binary' + },{}, this); + }, + function checkResponse(res) { + assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); + var ct = res.headers['content-type']; + assert.equal(ct, 'image/png'); + assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2, + function(err, similarity) { + if (err) throw err; + done(); + }); + } + ); + }); + // See http://github.com/Vizzuality/Windshaft-cartodb/issues/57 test("GET'ing a tile as anonymous with style set by POST", function(done){ var style = querystring.stringify({style: test_style_black_210, style_version: '2.1.0'}); From 2b2c22cdd50b8ea505581ee72e5a73c1f602f70b Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Thu, 28 Mar 2013 12:48:00 +0100 Subject: [PATCH 16/22] Add test showing use of mapnik subtitution tokens in multilayer config --- test/acceptance/multilayer.js | 169 +++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 2 deletions(-) diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 9691fff0..dd3cf312 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -165,12 +165,177 @@ suite('multilayer', function() { ); }); + + test("layergroup can hold substitution tokens", function(done) { + + var layergroup = { + version: '1.0.0', + layers: [ + { options: { + sql: 'select 1 as cartodb_id, ' + + 'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator', + cartocss: '#layer { polygon-fill:red; }', + cartocss_version: '2.0.1' + } } + ] + }; + + var expected_token; // = "d442ca6d3ece793b9c16c02a1d1ea5f2"; + Step( + function do_post() + { + var next = this; + assert.response(server, { + url: '/tiles/layergroup', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(layergroup) + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + var parsedBody = JSON.parse(res.body); + var expectedBody = { layergroupid: expected_token }; + // check last modified + var qTables = JSON.stringify({ + 'q': 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + + '$windshaft$)' + }); + expectedBody.last_updated = JSON.stringify({ + 'q': 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max ' + + 'FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{' + + qTables + '}\')' + }); + if ( expected_token ) { + //assert.equal(parsedBody.layergroupid, expectedBody.layergroupid); + //assert.equal(parsedBody.last_updated, expectedBody.last_updated); + assert.deepEqual(parsedBody, expectedBody); + } + else expected_token = parsedBody.layergroupid; + next(null, res); + }); + }, + function do_get_tile1(err) + { + if ( err ) throw err; + var next = this; + assert.response(server, { + url: '/tiles/layergroup/' + expected_token + '/1/0/0.png', + method: 'GET', + headers: {host: 'localhost' }, + encoding: 'binary' + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + assert.equal(res.headers['content-type'], "image/png"); + + // Check X-Cache-Channel + var cc = res.headers['x-cache-channel']; + assert.ok(cc); + var dbname = 'cartodb_test_user_1_db' + assert.equal(cc.substring(0, dbname.length), dbname); + var jsonquery = cc.substring(dbname.length+1); + var sentquery = JSON.parse(jsonquery); + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + + '$windshaft$)'); + + assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2, + function(err, similarity) { + next(err); + }); + }); + }, + function do_get_tile4(err) + { + if ( err ) throw err; + var next = this; + assert.response(server, { + url: '/tiles/layergroup/' + expected_token + '/4/0/0.png', + method: 'GET', + headers: {host: 'localhost' }, + encoding: 'binary' + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + assert.equal(res.headers['content-type'], "image/png"); + + // Check X-Cache-Channel + var cc = res.headers['x-cache-channel']; + assert.ok(cc); + var dbname = 'cartodb_test_user_1_db' + assert.equal(cc.substring(0, dbname.length), dbname); + var jsonquery = cc.substring(dbname.length+1); + var sentquery = JSON.parse(jsonquery); + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + + '$windshaft$)'); + + assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2, + function(err, similarity) { + next(err); + }); + }); + }, + function do_get_grid1(err) + { + if ( err ) throw err; + var next = this; + assert.response(server, { + url: '/tiles/layergroup/' + expected_token + + '/layer0/1/0/0.grid.json?interactivity=cartodb_id', + headers: {host: 'localhost' }, + method: 'GET' + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8"); + assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2, + function(err, similarity) { + next(err); + }); + }); + }, + function do_get_grid4(err) + { + if ( err ) throw err; + var next = this; + assert.response(server, { + url: '/tiles/layergroup/' + expected_token + + '/layer0/4/0/0.grid.json?interactivity=cartodb_id', + headers: {host: 'localhost' }, + method: 'GET' + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8"); + assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2, + function(err, similarity) { + next(err); + }); + }); + }, + function finish(err) { + var errors = []; + if ( err ) { + errors.push(err.message); + console.log("Error: " + err); + } + redis_client.keys("map_style|cartodb_test_user_1_db|~" + expected_token, function(err, matches) { + if ( err ) errors.push(err.message); + assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches); + redis_client.del(matches, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) done(new Error(errors)); + else done(null); + }); + }); + } + ); + }); + suiteTeardown(function(done) { // This test will add map_style records, like // 'map_style|null|publicuser|my_table', redis_client.keys("map_style|*", function(err, matches) { - _.each(matches, function(k) { redis_client.del(k); }); - sqlapi_server.close(done); + redis_client.del(matches, function(err) { + sqlapi_server.close(done); + }); }); }); From aea107f1af92f769557ea23c9213b99a3f001c67 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 29 Mar 2013 16:32:26 +0100 Subject: [PATCH 17/22] Upgrade Windshaft to 0.10.0, changing multilayer interface WARNING: starting from this commit the grid fetching route changed to NOT include layer name nor interactivity (which is now specified solely as part of layergroup configuration) Target 1.2.0 release --- NEWS.md | 8 +- npm-shrinkwrap.json | 158 ++++++++++++++++++---------------- package.json | 5 +- test/acceptance/multilayer.js | 126 ++++++++++++++++++++++++--- 4 files changed, 211 insertions(+), 86 deletions(-) diff --git a/NEWS.md b/NEWS.md index c3d841c9..f8fdf452 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,17 @@ -1.1.9 +1.2.0 ----- + +WARNING: starting from this commit the grid fetching route changed + to NOT include layer name nor interactivity (which is now + specified solely as part of layergroup configuration) + * Handle SQL API errors by requesting no Varnish cache * Fix X-Cache-Channel for multilayer (by token) responses * Add last_modified field to POST layergroup response (#72) * Deprecate signal handler for USR1, add handler for USR2 (#71) * Fix support for ampersend characters in CartoCSS * Add support for LZMA compressed GET parameters +* Remove interactivity parameters from grid fetching route 1.1.8 ----- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cb895d39..e63efc57 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -13,7 +13,7 @@ "version": "1.9.2", "dependencies": { "formidable": { - "version": "1.0.12" + "version": "1.0.13" } } }, @@ -32,10 +32,10 @@ "version": "0.8.3" }, "npm": { - "version": "1.2.12", + "version": "1.2.15", "dependencies": { "semver": { - "version": "1.1.3" + "version": "1.1.4" }, "ini": { "version": "1.1.0" @@ -50,7 +50,7 @@ "version": "1.2.0" }, "minimatch": { - "version": "0.2.9", + "version": "0.2.11", "dependencies": { "sigmund": { "version": "1.0.0" @@ -71,7 +71,7 @@ "version": "1.0.5" }, "tar": { - "version": "0.1.16" + "version": "0.1.17" }, "fstream": { "version": "0.1.22" @@ -84,7 +84,7 @@ "from": "git://github.com/isaacs/inherits" }, "mkdirp": { - "version": "0.3.4" + "version": "0.3.5" }, "read": { "version": "1.0.4", @@ -95,13 +95,13 @@ } }, "lru-cache": { - "version": "2.0.4" + "version": "2.3.0" }, "node-gyp": { - "version": "0.8.4" + "version": "0.9.3" }, "fstream-npm": { - "version": "0.1.3", + "version": "0.1.4", "dependencies": { "fstream-ignore": { "version": "0.0.6" @@ -124,7 +124,7 @@ "version": "0.1.2" }, "npm-registry-client": { - "version": "0.2.17", + "version": "0.2.18", "dependencies": { "couch-login": { "version": "0.1.15" @@ -132,13 +132,13 @@ } }, "read-package-json": { - "version": "0.2.0" + "version": "0.3.0" }, "read-installed": { "version": "0.1.1" }, "glob": { - "version": "3.1.20" + "version": "3.1.21" }, "init-package-json": { "version": "0.0.6", @@ -175,6 +175,9 @@ }, "opener": { "version": "1.3.0" + }, + "chmodr": { + "version": "0.1.0" } } } @@ -186,82 +189,82 @@ "underscore": { "version": "1.3.3" }, - "grainstore": { - "version": "0.11.2", + "windshaft": { + "version": "0.10.0", "dependencies": { - "carto": { - "version": "0.9.3-cdb3", - "from": "git://github.com/CartoDB/carto.git#cdb-0.9.3-cdb3", + "grainstore": { + "version": "0.12.0", "dependencies": { - "mapnik-reference": { - "version": "5.0.0-cdb1", - "from": "git://github.com/CartoDB/mapnik-reference.git#cdb-5.0" - }, - "xml2js": { - "version": "0.1.14", + "carto": { + "version": "0.9.3-cdb3", + "from": "git://github.com/CartoDB/carto.git#cdb-0.9.3-cdb3", "dependencies": { - "sax": { - "version": "0.5.2" + "mapnik-reference": { + "version": "5.0.0-cdb1", + "from": "git://github.com/CartoDB/mapnik-reference.git#cdb-5.0" + }, + "xml2js": { + "version": "0.1.14", + "dependencies": { + "sax": { + "version": "0.5.2" + } + } } } - } - } - }, - "mapnik-reference": { - "version": "5.0.4" - }, - "millstone": { - "version": "0.5.15", - "dependencies": { - "generic-pool": { - "version": "2.0.3" }, - "request": { - "version": "2.12.0", + "mapnik-reference": { + "version": "5.0.4" + }, + "millstone": { + "version": "0.5.15", "dependencies": { - "form-data": { - "version": "0.0.3", + "generic-pool": { + "version": "2.0.3" + }, + "request": { + "version": "2.12.0", "dependencies": { - "combined-stream": { + "form-data": { "version": "0.0.3", "dependencies": { - "delayed-stream": { - "version": "0.0.5" + "combined-stream": { + "version": "0.0.3", + "dependencies": { + "delayed-stream": { + "version": "0.0.5" + } + } + }, + "async": { + "version": "0.1.9" } } }, - "async": { - "version": "0.1.9" + "mime": { + "version": "1.2.7" } } }, + "srs": { + "version": "0.2.20" + }, + "zipfile": { + "version": "0.3.4" + }, + "sqlite3": { + "version": "2.1.7" + }, "mime": { - "version": "1.2.7" + "version": "1.2.9" + }, + "mkdirp": { + "version": "0.3.5" } } - }, - "srs": { - "version": "0.2.20" - }, - "zipfile": { - "version": "0.3.4" - }, - "sqlite3": { - "version": "2.1.7" - }, - "mime": { - "version": "1.2.9" - }, - "mkdirp": { - "version": "0.3.5" } } - } - } - }, - "windshaft": { - "version": "0.9.2", - "dependencies": { + }, "express": { "version": "2.5.11", "dependencies": { @@ -269,7 +272,7 @@ "version": "1.9.2", "dependencies": { "formidable": { - "version": "1.0.12" + "version": "1.0.13" } } }, @@ -301,12 +304,18 @@ } }, "tilelive-mapnik": { - "version": "0.3.3-cdb2", - "from": "git://github.com/Vizzuality/tilelive-mapnik.git#6061c65a", + "version": "0.5.0", + "from": "git://github.com/Vizzuality/tilelive-mapnik.git#6a360ee50", "dependencies": { + "generic-pool": { + "version": "2.0.3" + }, "eio": { "version": "0.1.0" }, + "mime": { + "version": "1.2.9" + }, "sphericalmercator": { "version": "1.0.2" } @@ -324,7 +333,12 @@ "version": "0.7.2" }, "hiredis": { - "version": "0.1.14" + "version": "0.1.15", + "dependencies": { + "bindings": { + "version": "1.1.0" + } + } }, "request": { "version": "2.9.202" @@ -336,7 +350,7 @@ "version": "1.2.2" }, "semver": { - "version": "1.1.3" + "version": "1.1.4" }, "mocha": { "version": "1.2.1", diff --git a/package.json b/package.json index 71c6d619..54ce1ffd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.1.9", + "version": "1.2.0", "description": "A map tile server for CartoDB", "url": "https://github.com/Vizzuality/Windshaft-cartodb", "licenses": [{ @@ -21,8 +21,7 @@ "cluster2": "git://github.com/CartoDB/cluster2.git#cdb_production", "node-varnish": "0.1.1", "underscore" : "~1.3.3", - "grainstore" : "~0.11.2", - "windshaft" : "~0.9.2", + "windshaft" : "~0.10.0", "step": "0.0.x", "generic-pool": "~1.0.12", "redis": "0.7.2", diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index dd3cf312..a08877c3 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -26,7 +26,7 @@ suite('multilayer', function() { sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); }); - test("layergroup with 2 layers, each with its style", function(done) { + test("layergroup with 2 layers, each with its style, grid in layer 0", function(done) { var layergroup = { version: '1.0.0', @@ -34,7 +34,8 @@ suite('multilayer', function() { { options: { sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator from test_table limit 2', cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }', - cartocss_version: '2.0.1' + cartocss_version: '2.0.1', + interactivity: [ 'cartodb_id' ] } }, { options: { sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator from test_table limit 2 offset 2', @@ -44,7 +45,7 @@ suite('multilayer', function() { ] }; - var expected_token = "d442ca6d3ece793b9c16c02a1d1ea5f2"; + var expected_token = "9b8b9742efba192ce6c534c83cba0186"; Step( function do_post() { @@ -116,7 +117,7 @@ suite('multilayer', function() { var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/layer0/0/0/0.grid.json?interactivity=cartodb_id', + + '/0/0/0.grid.json', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { @@ -128,13 +129,118 @@ suite('multilayer', function() { }); }); }, + function finish(err) { + var errors = []; + if ( err ) { + errors.push(err.message); + console.log("Error: " + err); + } + redis_client.keys("map_style|cartodb_test_user_1_db|~" + expected_token, function(err, matches) { + if ( err ) errors.push(err.message); + assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches); + redis_client.del(matches, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) done(new Error(errors)); + else done(null); + }); + }); + } + ); + }); + + test("layergroup with 2 layers, each with its style, grid in layer 1", function(done) { + + var layergroup = { + version: '1.0.0', + layers: [ + { options: { + sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator from test_table limit 2', + cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }', + cartocss_version: '2.0.1', + interactivity: [ 'fake' ] // unused, not being the topmost + } }, + { options: { + sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator from test_table limit 2 offset 2', + cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }', + cartocss_version: '2.0.2', + interactivity: [ 'cartodb_id' ] + } } + ] + }; + + var expected_token = "20c5377296dee4124e6fd2f72309c6eb"; + Step( + function do_post() + { + var next = this; + assert.response(server, { + url: '/tiles/layergroup', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(layergroup) + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + var parsedBody = JSON.parse(res.body); + var expectedBody = { layergroupid: expected_token }; + // check last modified + var qTables = JSON.stringify({ + 'q': 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + ';' + + layergroup.layers[1].options.sql + + '$windshaft$)' + }); + expectedBody.last_updated = JSON.stringify({ + 'q': 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max ' + + 'FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{' + + qTables + '}\')' + }); + if ( expected_token ) { + //assert.equal(parsedBody.layergroupid, expectedBody.layergroupid); + //assert.equal(parsedBody.last_updated, expectedBody.last_updated); + assert.deepEqual(parsedBody, expectedBody); + } + else expected_token = parsedBody.layergroupid; + next(null, res); + }); + }, + function do_get_tile(err) + { + if ( err ) throw err; + var next = this; + assert.response(server, { + url: '/tiles/layergroup/' + expected_token + '/0/0/0.png', + method: 'GET', + headers: {host: 'localhost' }, + encoding: 'binary' + }, {}, function(res) { + assert.equal(res.statusCode, 200, res.body); + assert.equal(res.headers['content-type'], "image/png"); + + // Check X-Cache-Channel + var cc = res.headers['x-cache-channel']; + assert.ok(cc); + var dbname = 'cartodb_test_user_1_db' + assert.equal(cc.substring(0, dbname.length), dbname); + var jsonquery = cc.substring(dbname.length+1); + var sentquery = JSON.parse(jsonquery); + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + + layergroup.layers[0].options.sql + ';' + + layergroup.layers[1].options.sql + + '$windshaft$)'); + + assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2, + function(err, similarity) { + next(err); + }); + }); + }, function do_get_grid_layer1(err) { if ( err ) throw err; var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/layer1/0/0/0.grid.json?interactivity=cartodb_id', + + '/0/0/0.grid.json', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { @@ -165,7 +271,6 @@ suite('multilayer', function() { ); }); - test("layergroup can hold substitution tokens", function(done) { var layergroup = { @@ -175,12 +280,13 @@ suite('multilayer', function() { sql: 'select 1 as cartodb_id, ' + 'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator', cartocss: '#layer { polygon-fill:red; }', - cartocss_version: '2.0.1' + cartocss_version: '2.0.1', + interactivity: ['cartodb_id'] } } ] }; - var expected_token; // = "d442ca6d3ece793b9c16c02a1d1ea5f2"; + var expected_token = "71c795a09d5e351c937a0b4bcbd867e5"; Step( function do_post() { @@ -280,7 +386,7 @@ suite('multilayer', function() { var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/layer0/1/0/0.grid.json?interactivity=cartodb_id', + + '/1/0/0.grid.json', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { @@ -298,7 +404,7 @@ suite('multilayer', function() { var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/layer0/4/0/0.grid.json?interactivity=cartodb_id', + + '/4/0/0.grid.json', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { From 3383c44eb73cbbbfde1f70393c7d237fcbfabef7 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Fri, 29 Mar 2013 18:24:08 +0100 Subject: [PATCH 18/22] Require interactivity param in single-layer grid fetching request Closes #69 --- NEWS.md | 9 +++++---- lib/cartodb/server_options.js | 3 ++- test/acceptance/server.js | 30 +++++++++++++++++++++------- test/unit/cartodb/req2params.test.js | 18 ++++++++++++++--- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/NEWS.md b/NEWS.md index f8fdf452..9c9a7fec 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,9 @@ 1.2.0 ----- -WARNING: starting from this commit the grid fetching route changed - to NOT include layer name nor interactivity (which is now - specified solely as part of layergroup configuration) +WARNING: starting from this commit the multilayer grid fetching + route changed to NOT include layer name nor interactivity + (now specified solely as part of layergroup configuration) * Handle SQL API errors by requesting no Varnish cache * Fix X-Cache-Channel for multilayer (by token) responses @@ -11,7 +11,8 @@ WARNING: starting from this commit the grid fetching route changed * Deprecate signal handler for USR1, add handler for USR2 (#71) * Fix support for ampersend characters in CartoCSS * Add support for LZMA compressed GET parameters -* Remove interactivity parameters from grid fetching route +* Remove interactivity config from multilayer grid fetching route +* Require interactivity param in single-layer grid fetching request (#69) 1.1.8 ----- diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index abd60c6f..a86e7d66 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -303,7 +303,8 @@ module.exports = function(){ _.extend(req.params, req.query); // for cartodb, ensure interactivity is cartodb_id or user specified - req.params.interactivity = req.params.interactivity || 'cartodb_id'; + // Don't: https://github.com/Vizzuality/Windshaft-cartodb/issues/69 + //req.params.interactivity = req.params.interactivity || 'cartodb_id'; req.params.processXML = function(req, xml, callback) { var dbuser = req.dbuser ? req.dbuser : global.settings.postgres.user; diff --git a/test/acceptance/server.js b/test/acceptance/server.js index cddc1cd4..0fa3ea12 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -530,11 +530,24 @@ suite('server', function() { // ///////////////////////////////////////////////////////////////////////////////// - test("get'ing a json with default style should return an grid", function(done){ + test("get'ing a grid with no interactivity should fail", function(done){ assert.response(server, { headers: {host: 'localhost'}, url: '/tiles/gadm4/6/31/24.grid.json', method: 'GET' + },{}, function(res) { + assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body); + assert.deepEqual(JSON.parse(res.body), {"error":"Missing interactivity parameter"}); + done(); + }); + }); + + + test("get'ing a json with default style should return an grid", function(done){ + assert.response(server, { + headers: {host: 'localhost'}, + url: '/tiles/gadm4/6/31/24.grid.json?interactivity=cartodb_id', + method: 'GET' },{ status: 200, headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8', @@ -545,7 +558,7 @@ suite('server', function() { test("get'ing a json with default style should return an grid", function(done){ assert.response(server, { headers: {host: 'localhost'}, - url: '/tiles/gadm4/6/31/24.grid.json', + url: '/tiles/gadm4/6/31/24.grid.json?interactivity=cartodb_id', method: 'GET' },{ status: 200, @@ -554,10 +567,13 @@ suite('server', function() { }); test("get'ing a json with default style and sql should return a constrained grid", function(done){ - var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"}) + var q = querystring.stringify({ + interactivity: 'cartodb_id', + sql: "SELECT * FROM gadm4 WHERE codineprov = '08'" + }) assert.response(server, { headers: {host: 'localhost'}, - url: '/tiles/gadm4/6/31/24.grid.json?' + sql, + url: '/tiles/gadm4/6/31/24.grid.json?' + q, method: 'GET' },{ status: 200, @@ -569,7 +585,7 @@ suite('server', function() { function(done) { assert.response(server, { headers: {host: 'localhost'}, - url: '/tiles/test_table_private_1/6/31/24.grid.json', + url: '/tiles/test_table_private_1/6/31/24.grid.json?interactivity=cartodb_id', method: 'GET' },{}, function(res) { // 401 Unauthorized @@ -583,7 +599,7 @@ suite('server', function() { function(done) { assert.response(server, { headers: {host: 'unknown_user'}, - url: '/tiles/test_table_private_1/6/31/24.grid.json', + url: '/tiles/test_table_private_1/6/31/24.grid.json?interactivity=cartodb_id', method: 'GET' },{ }, function(res) { @@ -599,7 +615,7 @@ suite('server', function() { function(done) { assert.response(server, { headers: {host: 'localhost'}, - url: '/tiles/test_table_private_1/6/31/24.grid.json?map_key=1234', + url: '/tiles/test_table_private_1/6/31/24.grid.json?map_key=1234&interactivity=cartodb_id', method: 'GET' },{}, function(res) { assert.equal(res.statusCode, 200, res.body); diff --git a/test/unit/cartodb/req2params.test.js b/test/unit/cartodb/req2params.test.js index 2562b076..a29e9708 100644 --- a/test/unit/cartodb/req2params.test.js +++ b/test/unit/cartodb/req2params.test.js @@ -19,7 +19,7 @@ suite('req2params', function() { assert.ok(_.isObject(req.query), 'request has query'); assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); assert.ok(req.hasOwnProperty('params'), 'request has params'); - assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity'); + assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params have interactivity'); assert.equal(req.params.dbname, 'cartodb_test_user_1_db', 'could forge dbname: '+ req.params.dbname); assert.ok(!req.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')'); done(); @@ -33,7 +33,7 @@ suite('req2params', function() { assert.ok(_.isObject(req.query), 'request has query'); assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); assert.ok(req.hasOwnProperty('params'), 'request has params'); - assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity'); + assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params have interactivity'); // database_name for user "localhost" (see test/support/prepare_db.sh) assert.equal(req.params.dbname, 'cartodb_test_user_1_db'); // unauthenticated request gets no dbuser @@ -49,7 +49,7 @@ suite('req2params', function() { assert.ok(_.isObject(req.query), 'request has query'); assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); assert.ok(req.hasOwnProperty('params'), 'request has params'); - assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity'); + assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params have interactivity'); // database_name for user "localhost" (see test/support/prepare_db.sh) assert.equal(req.params.dbname, 'cartodb_test_user_1_db'); // id for user "localhost" (see test/support/prepare_db.sh) @@ -62,5 +62,17 @@ suite('req2params', function() { }); }); }); + + test('retains interactivity', function(done){ + opts.req2params({headers: { host:'localhost' }, query: {interactivity: 'fld'} }, function(err, req) { + if ( err ) { console.log(err); throw new Error(err); } + //console.dir(req); + assert.ok(_.isObject(req.query), 'request has query'); + assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query'); + assert.ok(req.hasOwnProperty('params'), 'request has params'); + assert.ok(req.params.hasOwnProperty('interactivity'), 'request params has no interactivity'); + done(); + }); + }); }); From 4329ffd61a893ce7aca182a72481ccdf537decad Mon Sep 17 00:00:00 2001 From: Luis Bosque Date: Tue, 2 Apr 2013 11:42:21 +0200 Subject: [PATCH 19/22] Target v1.2.1 --- NEWS.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 9c9a7fec..24366d53 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +1.2.1 +----- + 1.2.0 ----- diff --git a/package.json b/package.json index 54ce1ffd..b25456af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.2.0", + "version": "1.2.1", "description": "A map tile server for CartoDB", "url": "https://github.com/Vizzuality/Windshaft-cartodb", "licenses": [{ From 106e95940d9d71b5c1bb0e235e4413fab6d2051e Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Tue, 2 Apr 2013 12:24:03 +0200 Subject: [PATCH 20/22] Revert backward incompatible changes in multilayer handling --- NEWS.md | 8 +-- package.json | 2 +- test/acceptance/multilayer.js | 126 +++------------------------------- 3 files changed, 12 insertions(+), 124 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9c9a7fec..ad13f4c6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,18 +1,12 @@ -1.2.0 +1.1.9 ----- -WARNING: starting from this commit the multilayer grid fetching - route changed to NOT include layer name nor interactivity - (now specified solely as part of layergroup configuration) - * Handle SQL API errors by requesting no Varnish cache * Fix X-Cache-Channel for multilayer (by token) responses * Add last_modified field to POST layergroup response (#72) * Deprecate signal handler for USR1, add handler for USR2 (#71) * Fix support for ampersend characters in CartoCSS * Add support for LZMA compressed GET parameters -* Remove interactivity config from multilayer grid fetching route -* Require interactivity param in single-layer grid fetching request (#69) 1.1.8 ----- diff --git a/package.json b/package.json index 54ce1ffd..b68cdd8b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "cluster2": "git://github.com/CartoDB/cluster2.git#cdb_production", "node-varnish": "0.1.1", "underscore" : "~1.3.3", - "windshaft" : "~0.10.0", + "windshaft" : "~0.11.0", "step": "0.0.x", "generic-pool": "~1.0.12", "redis": "0.7.2", diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index a08877c3..259c0c6e 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -26,7 +26,7 @@ suite('multilayer', function() { sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); }); - test("layergroup with 2 layers, each with its style, grid in layer 0", function(done) { + test("layergroup with 2 layers, each with its style", function(done) { var layergroup = { version: '1.0.0', @@ -34,8 +34,7 @@ suite('multilayer', function() { { options: { sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator from test_table limit 2', cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }', - cartocss_version: '2.0.1', - interactivity: [ 'cartodb_id' ] + cartocss_version: '2.0.1' } }, { options: { sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator from test_table limit 2 offset 2', @@ -45,7 +44,7 @@ suite('multilayer', function() { ] }; - var expected_token = "9b8b9742efba192ce6c534c83cba0186"; + var expected_token = "d55208dccb30b5ff972562f563db0d22"; Step( function do_post() { @@ -117,7 +116,7 @@ suite('multilayer', function() { var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/0/0/0.grid.json', + + '/layer0/0/0/0.grid.json?interactivity=cartodb_id', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { @@ -129,118 +128,13 @@ suite('multilayer', function() { }); }); }, - function finish(err) { - var errors = []; - if ( err ) { - errors.push(err.message); - console.log("Error: " + err); - } - redis_client.keys("map_style|cartodb_test_user_1_db|~" + expected_token, function(err, matches) { - if ( err ) errors.push(err.message); - assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches); - redis_client.del(matches, function(err) { - if ( err ) errors.push(err.message); - if ( errors.length ) done(new Error(errors)); - else done(null); - }); - }); - } - ); - }); - - test("layergroup with 2 layers, each with its style, grid in layer 1", function(done) { - - var layergroup = { - version: '1.0.0', - layers: [ - { options: { - sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator from test_table limit 2', - cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }', - cartocss_version: '2.0.1', - interactivity: [ 'fake' ] // unused, not being the topmost - } }, - { options: { - sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator from test_table limit 2 offset 2', - cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }', - cartocss_version: '2.0.2', - interactivity: [ 'cartodb_id' ] - } } - ] - }; - - var expected_token = "20c5377296dee4124e6fd2f72309c6eb"; - Step( - function do_post() - { - var next = this; - assert.response(server, { - url: '/tiles/layergroup', - method: 'POST', - headers: {host: 'localhost', 'Content-Type': 'application/json' }, - data: JSON.stringify(layergroup) - }, {}, function(res) { - assert.equal(res.statusCode, 200, res.body); - var parsedBody = JSON.parse(res.body); - var expectedBody = { layergroupid: expected_token }; - // check last modified - var qTables = JSON.stringify({ - 'q': 'SELECT CDB_QueryTables($windshaft$' - + layergroup.layers[0].options.sql + ';' - + layergroup.layers[1].options.sql - + '$windshaft$)' - }); - expectedBody.last_updated = JSON.stringify({ - 'q': 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max ' - + 'FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{' - + qTables + '}\')' - }); - if ( expected_token ) { - //assert.equal(parsedBody.layergroupid, expectedBody.layergroupid); - //assert.equal(parsedBody.last_updated, expectedBody.last_updated); - assert.deepEqual(parsedBody, expectedBody); - } - else expected_token = parsedBody.layergroupid; - next(null, res); - }); - }, - function do_get_tile(err) - { - if ( err ) throw err; - var next = this; - assert.response(server, { - url: '/tiles/layergroup/' + expected_token + '/0/0/0.png', - method: 'GET', - headers: {host: 'localhost' }, - encoding: 'binary' - }, {}, function(res) { - assert.equal(res.statusCode, 200, res.body); - assert.equal(res.headers['content-type'], "image/png"); - - // Check X-Cache-Channel - var cc = res.headers['x-cache-channel']; - assert.ok(cc); - var dbname = 'cartodb_test_user_1_db' - assert.equal(cc.substring(0, dbname.length), dbname); - var jsonquery = cc.substring(dbname.length+1); - var sentquery = JSON.parse(jsonquery); - assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' - + layergroup.layers[0].options.sql + ';' - + layergroup.layers[1].options.sql - + '$windshaft$)'); - - assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2, - function(err, similarity) { - next(err); - }); - }); - }, function do_get_grid_layer1(err) { if ( err ) throw err; var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/0/0/0.grid.json', + + '/layer1/0/0/0.grid.json?interactivity=cartodb_id', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { @@ -271,6 +165,7 @@ suite('multilayer', function() { ); }); + test("layergroup can hold substitution tokens", function(done) { var layergroup = { @@ -280,13 +175,12 @@ suite('multilayer', function() { sql: 'select 1 as cartodb_id, ' + 'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator', cartocss: '#layer { polygon-fill:red; }', - cartocss_version: '2.0.1', - interactivity: ['cartodb_id'] + cartocss_version: '2.0.1' } } ] }; - var expected_token = "71c795a09d5e351c937a0b4bcbd867e5"; + var expected_token = "20f5710c00e3a1b0b4950de65ef0d875"; Step( function do_post() { @@ -386,7 +280,7 @@ suite('multilayer', function() { var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/1/0/0.grid.json', + + '/layer0/1/0/0.grid.json?interactivity=cartodb_id', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { @@ -404,7 +298,7 @@ suite('multilayer', function() { var next = this; assert.response(server, { url: '/tiles/layergroup/' + expected_token - + '/4/0/0.grid.json', + + '/layer0/4/0/0.grid.json?interactivity=cartodb_id', headers: {host: 'localhost' }, method: 'GET' }, {}, function(res) { From 113b70cf9804ae3b05044357093ec8a80c49bce7 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Tue, 2 Apr 2013 13:30:49 +0200 Subject: [PATCH 21/22] Add support for creating layergroups via GET --- NEWS.md | 3 ++- lib/cartodb/server_options.js | 3 +-- npm-shrinkwrap.json | 6 +++--- package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NEWS.md b/NEWS.md index ad13f4c6..78c528c5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,10 +3,11 @@ * Handle SQL API errors by requesting no Varnish cache * Fix X-Cache-Channel for multilayer (by token) responses -* Add last_modified field to POST layergroup response (#72) +* Add last_modified field to layergroup creation response (#72) * Deprecate signal handler for USR1, add handler for USR2 (#71) * Fix support for ampersend characters in CartoCSS * Add support for LZMA compressed GET parameters +* Add support for creating layergroups via GET 1.1.8 ----- diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index a86e7d66..b4e47c43 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -216,9 +216,8 @@ module.exports = function(){ }); }; - me.afterLayergroupCreate = function(req, response, callback) { + me.afterLayergroupCreate = function(req, mapconfig, response, callback) { var token = response.layergroupid; - var mapconfig = req.body; var sql = []; _.each(mapconfig.layers, function(lyr) { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e63efc57..49c8c01f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -32,7 +32,7 @@ "version": "0.8.3" }, "npm": { - "version": "1.2.15", + "version": "1.2.16", "dependencies": { "semver": { "version": "1.1.4" @@ -98,7 +98,7 @@ "version": "2.3.0" }, "node-gyp": { - "version": "0.9.3" + "version": "0.9.5" }, "fstream-npm": { "version": "0.1.4", @@ -190,7 +190,7 @@ "version": "1.3.3" }, "windshaft": { - "version": "0.10.0", + "version": "0.11.0", "dependencies": { "grainstore": { "version": "0.12.0", diff --git a/package.json b/package.json index b68cdd8b..5da7c9dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.2.0", + "version": "1.1.9", "description": "A map tile server for CartoDB", "url": "https://github.com/Vizzuality/Windshaft-cartodb", "licenses": [{ From a0b6f467b1bee92d112f4a858eac6b6a47510dff Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 3 Apr 2013 11:10:09 +0200 Subject: [PATCH 22/22] HOTFIX: require windshaft-0.11.1 to drop tilelive internal cache --- npm-shrinkwrap.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 49c8c01f..f720c85a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -190,7 +190,7 @@ "version": "1.3.3" }, "windshaft": { - "version": "0.11.0", + "version": "0.11.1", "dependencies": { "grainstore": { "version": "0.12.0", diff --git a/package.json b/package.json index 5da7c9dd..e87e1a2c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "cluster2": "git://github.com/CartoDB/cluster2.git#cdb_production", "node-varnish": "0.1.1", "underscore" : "~1.3.3", - "windshaft" : "~0.11.0", + "windshaft" : "~0.11.1", "step": "0.0.x", "generic-pool": "~1.0.12", "redis": "0.7.2",