Merge remote-tracking branch 'origin/master' into layergroup-token-param
This commit is contained in:
commit
78404b1308
82
NEWS.md
82
NEWS.md
@ -1,8 +1,88 @@
|
||||
# Changelog
|
||||
|
||||
## 3.10.2
|
||||
## 3.12.11
|
||||
Released 2017-mm-dd
|
||||
|
||||
## 3.12.10
|
||||
Released 2017-09-18
|
||||
- Upgrades windshaft to [3.3.2](https://github.com/CartoDB/windshaft/releases/tag/3.3.2).
|
||||
|
||||
## 3.12.9
|
||||
Released 2017-09-07
|
||||
|
||||
Bug fixes:
|
||||
- Do not use distinct when calculating quantiles. #743
|
||||
|
||||
## 3.12.8
|
||||
Released 2017-09-07
|
||||
|
||||
Bug fixes:
|
||||
- Integer out of range in date histograms. (https://github.com/CartoDB/support/issues/962)
|
||||
|
||||
## 3.12.7
|
||||
Released 2017-09-01
|
||||
|
||||
- Upgrades camshaft to [0.58.1](https://github.com/CartoDB/camshaft/releases/tag/0.58.1).
|
||||
|
||||
|
||||
## 3.12.6
|
||||
Released 2017-08-31
|
||||
|
||||
- Upgrades camshaft to [0.58.0](https://github.com/CartoDB/camshaft/releases/tag/0.58.0).
|
||||
|
||||
|
||||
## 3.12.5
|
||||
Released 2017-08-24
|
||||
|
||||
- Upgrades camshaft to [0.57.0](https://github.com/CartoDB/camshaft/releases/tag/0.57.0).
|
||||
|
||||
|
||||
## 3.12.4
|
||||
Released 2017-08-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.56.0](https://github.com/CartoDB/camshaft/releases/tag/0.56.0).
|
||||
|
||||
## 3.12.3
|
||||
Released 2017-08-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.8](https://github.com/CartoDB/camshaft/releases/tag/0.55.8).
|
||||
|
||||
## 3.12.2
|
||||
Released 2017-08-16
|
||||
|
||||
Bug fixes:
|
||||
- Polygon count problems #725.
|
||||
|
||||
|
||||
## 3.12.1
|
||||
Released 2017-08-13
|
||||
- Upgrades cartodb-psql to [0.10.1](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.1).
|
||||
- Upgrades windshaft to [3.3.1](https://github.com/CartoDB/windshaft/releases/tag/3.3.1).
|
||||
- Upgrades camshaft to [0.55.7](https://github.com/CartoDB/camshaft/releases/tag/0.55.7).
|
||||
|
||||
|
||||
## 3.12.0
|
||||
Released 2017-08-10
|
||||
|
||||
Announcements:
|
||||
- Apply max tile response time for requests to layergoup, tiles, static maps, attributes and dataviews services #717.
|
||||
- Upgrades windshaft to [3.3.0](https://github.com/CartoDB/windshaft/releases/tag/3.3.0).
|
||||
- Upgrades cartodb-redis to [0.14.0](https://github.com/CartoDB/node-cartodb-redis/releases/tag/0.14.0).
|
||||
|
||||
|
||||
## 3.11.0
|
||||
Released 2017-08-08
|
||||
|
||||
Announcements:
|
||||
- Allow to override with any aggregation for histograms instantiated w/o aggregation.
|
||||
|
||||
Bug fixes:
|
||||
- Apply timezone after truncating the minimun date for each bin to calculate timestamps in time-series.
|
||||
- Support timestamp with timezones to calculate the number of bins in time-series.
|
||||
- Fixed issue related to name collision while building time-series query.
|
||||
|
||||
|
||||
## 3.10.1
|
||||
Released 2017-08-04
|
||||
|
@ -1,3 +1,5 @@
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param metadataBackend
|
||||
@ -13,16 +15,65 @@ function UserLimitsApi(metadataBackend, options) {
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
|
||||
|
||||
var limits = {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: self.options.limits.render || 0
|
||||
};
|
||||
|
||||
self.getTimeoutRenderLimit(username, apiKey, function (err, timeoutRenderLimit) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || self.options.limits.render || 0
|
||||
});
|
||||
if (timeoutRenderLimit && timeoutRenderLimit.render) {
|
||||
if (Number.isFinite(timeoutRenderLimit.render)) {
|
||||
limits.render = timeoutRenderLimit.render;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, limits);
|
||||
});
|
||||
};
|
||||
|
||||
UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function isAuthorized() {
|
||||
var next = this;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(null, false);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserMapKey(username, function (err, userApiKey) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(null, userApiKey === apiKey);
|
||||
});
|
||||
},
|
||||
function getUserTimeoutRenderLimits(err, authorized) {
|
||||
var next = this;
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserTimeoutRenderLimits(username, function (err, timeoutRenderLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, {
|
||||
render: authorized ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
|
||||
});
|
||||
});
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
@ -21,10 +21,10 @@ function createTemplate(method) {
|
||||
}
|
||||
|
||||
var methods = {
|
||||
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
|
||||
quantiles: 'CDB_QuantileBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as quantiles',
|
||||
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
|
||||
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
|
||||
jenks: 'CDB_JenksBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as headtails'
|
||||
};
|
||||
|
||||
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {
|
||||
|
@ -187,6 +187,9 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
|
||||
BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
|
||||
allErrors = populateTimeoutErrors(allErrors);
|
||||
|
||||
label = label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
@ -221,6 +224,18 @@ function stripConnectionInfo(message) {
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
var ERROR_INFO_TO_EXPOSE = {
|
||||
message: true,
|
||||
layer: true,
|
||||
type: true,
|
||||
analysis: true,
|
||||
subtype: true
|
||||
};
|
||||
|
||||
function shouldBeExposed (prop) {
|
||||
return !!ERROR_INFO_TO_EXPOSE[prop];
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
@ -239,7 +254,7 @@ function errorMessageWithContext(err) {
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop)) {
|
||||
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
@ -281,5 +296,38 @@ function statusFromErrorMessage(errMsg) {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function populateTimeoutErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isRenderTimeoutError(error)) {
|
||||
error.subtype = 'render';
|
||||
}
|
||||
|
||||
if (isDatasourceTimeoutError(error)) {
|
||||
error.subtype = 'datasource';
|
||||
}
|
||||
|
||||
if (isTimeoutError(error)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
}
|
||||
|
||||
psql.query(query, function(err, result) {
|
||||
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
@ -9,42 +9,42 @@ var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
var dateIntervalQueryTpl = dot.template([
|
||||
'WITH',
|
||||
'dates AS (',
|
||||
'__cdb_dates AS (',
|
||||
' SELECT',
|
||||
' MAX({{=it.column}}::timestamp) AS _end,',
|
||||
' MIN({{=it.column}}::timestamp) AS _start',
|
||||
' FROM ({{=it.query}}) _cdb_source',
|
||||
' MAX({{=it.column}}::timestamp) AS __cdb_end,',
|
||||
' MIN({{=it.column}}::timestamp) AS __cdb_start',
|
||||
' FROM ({{=it.query}}) __cdb_source',
|
||||
'),',
|
||||
'interval_in_days AS (',
|
||||
'__cdb_interval_in_days AS (',
|
||||
' SELECT' ,
|
||||
' DATE_PART(\'day\', _end - _start) AS days',
|
||||
' FROM dates',
|
||||
' DATE_PART(\'day\', __cdb_end - __cdb_start) AS __cdb_days',
|
||||
' FROM __cdb_dates',
|
||||
'),',
|
||||
'interval_in_hours AS (',
|
||||
'__cdb_interval_in_hours AS (',
|
||||
' SELECT',
|
||||
' days * 24 + DATE_PART(\'hour\', _end - _start) AS hours',
|
||||
' FROM interval_in_days, dates',
|
||||
' __cdb_days * 24 + DATE_PART(\'hour\', __cdb_end - __cdb_start) AS __cdb_hours',
|
||||
' FROM __cdb_interval_in_days, __cdb_dates',
|
||||
'),',
|
||||
'interval_in_minutes AS (',
|
||||
'__cdb_interval_in_minutes AS (',
|
||||
' SELECT',
|
||||
' hours * 60 + DATE_PART(\'minute\', _end - _start) AS minutes',
|
||||
' FROM interval_in_hours, dates',
|
||||
' __cdb_hours * 60 + DATE_PART(\'minute\', __cdb_end - __cdb_start) AS __cdb_minutes',
|
||||
' FROM __cdb_interval_in_hours, __cdb_dates',
|
||||
'),',
|
||||
'interval_in_seconds AS (',
|
||||
'__cdb_interval_in_seconds AS (',
|
||||
' SELECT',
|
||||
' minutes * 60 + DATE_PART(\'second\', _end - _start) AS seconds',
|
||||
' FROM interval_in_minutes, dates',
|
||||
' __cdb_minutes * 60 + DATE_PART(\'second\', __cdb_end - __cdb_start) AS __cdb_seconds',
|
||||
' FROM __cdb_interval_in_minutes, __cdb_dates',
|
||||
')',
|
||||
'SELECT',
|
||||
' ROUND(days / 365) AS year,',
|
||||
' ROUND(days / 90) AS quarter,',
|
||||
' ROUND(days / 30) AS month,',
|
||||
' ROUND(days / 7) AS week,',
|
||||
' days AS day,',
|
||||
' hours AS hour,',
|
||||
' minutes AS minute,',
|
||||
' seconds AS second',
|
||||
'FROM interval_in_days, interval_in_hours, interval_in_minutes, interval_in_seconds'
|
||||
' ROUND(__cdb_days / 365) AS year,',
|
||||
' ROUND(__cdb_days / 90) AS quarter,',
|
||||
' ROUND(__cdb_days / 30) AS month,',
|
||||
' ROUND(__cdb_days / 7) AS week,',
|
||||
' __cdb_days AS day,',
|
||||
' __cdb_hours AS hour,',
|
||||
' __cdb_minutes AS minute,',
|
||||
' __cdb_seconds AS second',
|
||||
'FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds'
|
||||
].join('\n'));
|
||||
|
||||
var MAX_INTERVAL_VALUE = 366;
|
||||
@ -52,9 +52,9 @@ var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var filteredQueryTpl = dot.template([
|
||||
'filtered_source AS (',
|
||||
'__cdb_filtered_source AS (',
|
||||
' SELECT *',
|
||||
' FROM ({{=it._query}}) _cdb_filtered_source',
|
||||
' FROM ({{=it._query}}) __cdb_filtered_source_query',
|
||||
' WHERE',
|
||||
' {{=it._column}} IS NOT NULL',
|
||||
' {{?it._isFloatColumn}}AND',
|
||||
@ -67,74 +67,74 @@ var filteredQueryTpl = dot.template([
|
||||
].join(' \n'));
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
'__cdb_basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM filtered_source',
|
||||
' max({{=it._column}}) AS __cdb_max_val, min({{=it._column}}) AS __cdb_min_val,',
|
||||
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
|
||||
' FROM __cdb_filtered_source',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
'__cdb_basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM filtered_source',
|
||||
' max({{=it._end}}) AS __cdb_max_val, min({{=it._start}}) AS __cdb_min_val,',
|
||||
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
|
||||
' FROM __cdb_filtered_source',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
'__cdb_iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM filtered_source) _cdb_quartiles',
|
||||
' FROM __cdb_filtered_source) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
' ) __cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
'__cdb_bins AS (',
|
||||
' SELECT CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST({{=it._minBins}}, CAST(__cdb_total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, filtered_source',
|
||||
' END AS __cdb_bins_number',
|
||||
' FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
'__cdb_bins AS (',
|
||||
' SELECT {{=it._bins}} AS __cdb_bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
'__cdb_nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' count(*) AS __cdb_nulls_count',
|
||||
' FROM ({{=it._query}}) __cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var infinitiesQueryTpl = dot.template([
|
||||
'infinities AS (',
|
||||
'__cdb_infinities AS (',
|
||||
' SELECT',
|
||||
' count(*) AS infinities_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_infinities',
|
||||
' count(*) AS __cdb_infinities_count',
|
||||
' FROM ({{=it._query}}) __cdb_infinities_query',
|
||||
' WHERE',
|
||||
' {{=it._column}} = \'infinity\'::float',
|
||||
' OR',
|
||||
@ -143,121 +143,128 @@ var infinitiesQueryTpl = dot.template([
|
||||
].join('\n'));
|
||||
|
||||
var nansQueryTpl = dot.template([
|
||||
'nans AS (',
|
||||
'__cdb_nans AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nans_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_infinities',
|
||||
' count(*) AS __cdb_nans_count',
|
||||
' FROM ({{=it._query}}) __cdb_nans_query',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' {{?it._isFloatColumn}}infinities_count,',
|
||||
' nans_count,{{?}}',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' (__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,',
|
||||
' __cdb_bins_number AS bins_number,',
|
||||
' __cdb_nulls_count AS nulls_count,',
|
||||
' {{?it._isFloatColumn}}__cdb_infinities_count AS infinities_count,',
|
||||
' __cdb_nans_count AS nans_count,{{?}}',
|
||||
' __cdb_avg_val AS avg_val,',
|
||||
' CASE WHEN __cdb_min_val = __cdb_max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' ELSE GREATEST(',
|
||||
' 1,',
|
||||
' LEAST(',
|
||||
' WIDTH_BUCKET({{=it._column}}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),',
|
||||
' __cdb_bins_number',
|
||||
' )',
|
||||
' ) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' avg({{=it._column}})::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM filtered_source, basics, nulls, bins{{?it._isFloatColumn}}, infinities, nans{{?}}',
|
||||
'FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls,',
|
||||
' __cdb_bins{{?it._isFloatColumn}}, __cdb_infinities, __cdb_nans{{?}}',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count,',
|
||||
' avg_val{{?it._isFloatColumn}}, infinities_count, nans_count{{?}}',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
var dateBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
'__cdb_basics AS (',
|
||||
' SELECT',
|
||||
' max(date_part(\'epoch\', {{=it._column}})) AS max_val,',
|
||||
' min(date_part(\'epoch\', {{=it._column}})) AS min_val,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}})) AS avg_val,',
|
||||
' max(date_part(\'epoch\', {{=it._column}})) AS __cdb_max_val,',
|
||||
' min(date_part(\'epoch\', {{=it._column}})) AS __cdb_min_val,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
|
||||
' min(date_trunc(',
|
||||
' \'{{=it._aggregation}}\', {{=it._column}} AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )) AS start_date,',
|
||||
' max({{=it._column}} AT TIME ZONE \'{{=it._offset}}\') AS end_date,',
|
||||
' count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )) AS __cdb_start_date,',
|
||||
' max({{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\') AS __cdb_end_date,',
|
||||
' count(1) AS __cdb_total_rows',
|
||||
' FROM ({{=it._query}}) __cdb_basics_query',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var dateOverrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
'__cdb_basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val,',
|
||||
' min({{=it._start}}) AS min_val,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}})) AS avg_val,',
|
||||
' max({{=it._end}})::float AS __cdb_max_val,',
|
||||
' min({{=it._start}})::float AS __cdb_min_val,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
|
||||
' min(',
|
||||
' date_trunc(',
|
||||
' \'{{=it._aggregation}}\',',
|
||||
' TO_TIMESTAMP({{=it._start}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )',
|
||||
' ) AS start_date,',
|
||||
' ) AS __cdb_start_date,',
|
||||
' max(',
|
||||
' TO_TIMESTAMP({{=it._end}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' ) AS end_date,',
|
||||
' count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
' ) AS __cdb_end_date,',
|
||||
' count(1) AS __cdb_total_rows',
|
||||
' FROM ({{=it._query}}) __cdb_basics_query',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var dateBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
'__cdb_bins AS (',
|
||||
' SELECT',
|
||||
' bins_array,',
|
||||
' ARRAY_LENGTH(bins_array, 1) AS bins_number',
|
||||
' __cdb_bins_array,',
|
||||
' ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number',
|
||||
' FROM (',
|
||||
' SELECT',
|
||||
' ARRAY(',
|
||||
' SELECT GENERATE_SERIES(',
|
||||
' start_date::timestamptz,',
|
||||
' end_date::timestamptz,',
|
||||
' __cdb_start_date::timestamptz,',
|
||||
' __cdb_end_date::timestamptz,',
|
||||
' {{?it._aggregation==="quarter"}}\'3 month\'{{??}}\'1 {{=it._aggregation}}\'{{?}}::interval',
|
||||
' )',
|
||||
' ) AS bins_array',
|
||||
' FROM basics',
|
||||
' ) _cdb_bins_array',
|
||||
' ) AS __cdb_bins_array',
|
||||
' FROM __cdb_basics',
|
||||
' ) __cdb_bins_array_query',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var dateHistogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' (__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,',
|
||||
' __cdb_bins_number AS bins_number,',
|
||||
' __cdb_nulls_count AS nulls_count,',
|
||||
' CASE WHEN __cdb_min_val = __cdb_max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(',
|
||||
' WIDTH_BUCKET(',
|
||||
' {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\',',
|
||||
' bins_array',
|
||||
' __cdb_bins_array',
|
||||
' ),',
|
||||
' bins_number',
|
||||
' __cdb_bins_number',
|
||||
' )) - 1',
|
||||
' END AS bin,',
|
||||
' min(',
|
||||
' date_part(',
|
||||
' \'epoch\', ',
|
||||
' date_trunc(',
|
||||
' \'{{=it._aggregation}}\', {{=it._column}}::timestamptz',
|
||||
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' ) AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )',
|
||||
' )::numeric AS timestamp,',
|
||||
' date_part(\'epoch\', start_date)::numeric AS timestamp_start,',
|
||||
' date_part(\'epoch\', __cdb_start_date)::numeric AS timestamp_start,',
|
||||
' min(date_part(\'epoch\', {{=it._column}}))::numeric AS min,',
|
||||
' max(date_part(\'epoch\', {{=it._column}}))::numeric AS max,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}}))::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, bins, nulls',
|
||||
'FROM ({{=it._query}}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls',
|
||||
'WHERE date_part(\'epoch\', {{=it._column}}) IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val, start_date',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
@ -328,12 +335,16 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
this._buildQuery(psql, override, callback);
|
||||
};
|
||||
|
||||
Histogram.prototype.isDateHistogram = function (override) {
|
||||
return this._columnType === 'date' && (this.aggregation !== undefined || override.aggregation !== undefined);
|
||||
};
|
||||
|
||||
Histogram.prototype._buildQuery = function (psql, override, callback) {
|
||||
var filteredQuery, basicsQuery, binsQuery;
|
||||
var _column = this.column;
|
||||
var _query = this.query;
|
||||
|
||||
if (this._columnType === 'date' && this.aggregation !== undefined) {
|
||||
if (this.isDateHistogram(override)) {
|
||||
return this._buildDateHistogramQuery(psql, override, callback);
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ var filterQueryTpl = dot.template([
|
||||
].join('\n'));
|
||||
|
||||
var bboxFilterTpl = dot.template(
|
||||
'{{=it._column}} && ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}})'
|
||||
'ST_Intersects({{=it._column}}, ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}}))'
|
||||
);
|
||||
|
||||
var LATITUDE_MAX_VALUE = 85.0511287798066;
|
||||
|
@ -26,7 +26,7 @@ CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
|
@ -27,7 +27,7 @@ MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
|
@ -114,7 +114,7 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
function prepareContextLimits(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
mapConfig = _mapConfig;
|
||||
self.userLimitsApi.getRenderLimits(self.owner, this);
|
||||
self.userLimitsApi.getRenderLimits(self.owner, self.params.api_key, this);
|
||||
},
|
||||
function cacheAndReturnMapConfig(err, renderLimits) {
|
||||
self.err = err;
|
||||
|
@ -118,8 +118,27 @@ module.exports = function(serverOptions) {
|
||||
var onTileErrorStrategy;
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function isRasterFormat (format) {
|
||||
return format === 'png' || format === 'jpg';
|
||||
}
|
||||
|
||||
if (isTimeoutError(err) && isRasterFormat(format)) {
|
||||
return callback(null, timeoutErrorTile, {
|
||||
'Content-Type': 'image/png',
|
||||
}, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
|
14
package.json
14
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "3.10.2",
|
||||
"version": "3.12.11",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@ -17,14 +17,16 @@
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>",
|
||||
"Carlos Matallín <matallo@carto.com>"
|
||||
"Carlos Matallín <matallo@carto.com>",
|
||||
"Daniel Garcia Aubert <dgaubert@carto.com>",
|
||||
"Mario de Frutos <mario.defrutos@carto.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "~1.14.0",
|
||||
"camshaft": "0.55.6",
|
||||
"cartodb-psql": "0.8.0",
|
||||
"camshaft": "0.58.1",
|
||||
"cartodb-psql": "0.10.1",
|
||||
"cartodb-query-tables": "0.2.0",
|
||||
"cartodb-redis": "0.13.2",
|
||||
"cartodb-redis": "0.14.0",
|
||||
"debug": "~2.2.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.13.3",
|
||||
@ -40,7 +42,7 @@
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.19.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "3.2.2",
|
||||
"windshaft": "3.3.2",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -332,6 +332,21 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should cast overridden start and end to float to avoid out of range errors ' + test.desc, function (done) {
|
||||
var params = {
|
||||
start: -2145916800,
|
||||
end: 1009843199
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview(test.dataviewId, params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return same histogram ' + test.desc, function (done) {
|
||||
var params = {
|
||||
@ -888,7 +903,6 @@ describe('histogram-dates: aggregation input value', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('histogram-dates: timestamp starts at epoch', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
@ -958,3 +972,221 @@ describe('histogram-dates: timestamp starts at epoch', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('histogram-dates: trunc timestamp for each bin respecting user\'s timezone', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
source: {
|
||||
id: "a0"
|
||||
},
|
||||
cartocss: "#points { marker-width: 10; marker-fill: red; }",
|
||||
cartocss_version: "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
timezone_epoch_histogram: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
timezone_epoch_histogram_tz: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: [
|
||||
'select null::geometry the_geom_webmercator, date AS d',
|
||||
'from generate_series(',
|
||||
'\'1970-01-01 00:00:00\'::timestamp,',
|
||||
'\'1970-01-01 01:59:00\'::timestamp,',
|
||||
' \'1 minute\'::interval',
|
||||
') date'
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'a1',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: [
|
||||
'select null::geometry the_geom_webmercator, date AS d',
|
||||
'from generate_series(',
|
||||
'\'1970-01-01 00:00:00\'::timestamptz,',
|
||||
'\'1970-01-01 01:59:00\'::timestamptz,',
|
||||
' \'1 minute\'::interval',
|
||||
') date'
|
||||
].join(' ')
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var dateHistogramsUseCases = [{
|
||||
desc: 'supporting timestamp with offset',
|
||||
dataviewId: 'timezone_epoch_histogram_tz'
|
||||
}, {
|
||||
desc: 'supporting timestamp without offset',
|
||||
dataviewId: 'timezone_epoch_histogram'
|
||||
}];
|
||||
|
||||
dateHistogramsUseCases.forEach(function (test) {
|
||||
it('should return histogram with two buckets ' + test.desc , function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
const override = {
|
||||
aggregation: 'day',
|
||||
offset: '-3600'
|
||||
};
|
||||
|
||||
this.testClient.getDataview(test.dataviewId, override, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
|
||||
var OFFSET_IN_MINUTES = -1 * 60; // GMT-01
|
||||
var initialTimestamp = '1969-12-31T00:00:00-01:00';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_IN_MINUTES)
|
||||
.add(index, override.aggregation)
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('histogram: be able to override with aggregation for histograms instantiated w/o aggregation', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
source: {
|
||||
id: "a0"
|
||||
},
|
||||
cartocss: "#points { marker-width: 10; marker-fill: red; }",
|
||||
cartocss_version: "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
timezone_epoch_histogram: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: [
|
||||
'select null::geometry the_geom_webmercator, date AS d',
|
||||
'from generate_series(',
|
||||
'\'1970-01-01 00:00:00\'::timestamp,',
|
||||
'\'1970-01-01 01:59:00\'::timestamp,',
|
||||
' \'1 minute\'::interval',
|
||||
') date'
|
||||
].join(' ')
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should apply aggregation to the histogram', function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
const override = {
|
||||
aggregation: 'day',
|
||||
offset: '-3600'
|
||||
};
|
||||
|
||||
this.testClient.getDataview('timezone_epoch_histogram', override, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
|
||||
var OFFSET_IN_MINUTES = -1 * 60; // GMT-01
|
||||
var initialTimestamp = '1969-12-31T00:00:00-01:00';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_IN_MINUTES)
|
||||
.add(index, override.aggregation)
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,311 +0,0 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('render limits', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
|
||||
var server;
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var user = 'localhost';
|
||||
|
||||
var pointSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" 'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator, 1 cartodb_id";
|
||||
var pointCartoCss = '#layer { marker-fill:red; }';
|
||||
var polygonSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" ST_Buffer('SRID=3857;POINT(0 0)'::geometry, 100000000) the_geom_webmercator, 1 cartodb_id";
|
||||
var polygonCartoCss = '#layer { polygon-fill:red; }';
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost) {
|
||||
return {
|
||||
url: layergroupUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
function withRenderLimit(user, renderLimit, callback) {
|
||||
redisClient.SELECT(5, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var userLimitsKey = 'limits:tiler:' + user;
|
||||
redisClient.HSET(userLimitsKey, 'render', renderLimit, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
keysToDelete[userLimitsKey] = 5;
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategyEnabled;
|
||||
before(function() {
|
||||
onTileErrorStrategyEnabled = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategyEnabled;
|
||||
});
|
||||
|
||||
it("layergroup creation fails if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed.errors, [ 'Render timed out' ]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation does not fail if user limit is high enough even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("layergroup creation works if test tile is fast but tile request fails if they are slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed.errors, ['Render timed out']);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("tile request does not fail if user limit is high enough", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy', function() {
|
||||
|
||||
it("layergroup creation works even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation and tile requests works even if they are slow but returns fallback", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
var referenceImagePath = './test/fixtures/render-timeout-fallback.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -24,14 +24,24 @@ describe('mvt', function () {
|
||||
desc: 'should get empty mvt with code 204 (no content)',
|
||||
coords: { z: 0, x: 0, y: 0 },
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Content-Type': undefined
|
||||
}
|
||||
},
|
||||
mapConfig: createMapConfig(TestClient.SQL.EMPTY)
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile with code 200 (ok)',
|
||||
coords: { z: 0, x: 0, y: 0 },
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-protobuf'
|
||||
}
|
||||
},
|
||||
mapConfig: createMapConfig()
|
||||
}
|
||||
];
|
||||
@ -40,12 +50,12 @@ describe('mvt', function () {
|
||||
it(test.desc, done => {
|
||||
const testClient = new TestClient(test.mapConfig, 1234);
|
||||
const { z, x, y } = test.coords;
|
||||
const { format, status } = test;
|
||||
const { format, response } = test;
|
||||
|
||||
testClient.getTile(z, x, y, { format, status }, (err, res) => {
|
||||
testClient.getTile(z, x, y, { format, response }, (err, res) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, test.status);
|
||||
assert.equal(res.statusCode, test.response.status);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
786
test/acceptance/user-database-timeout-limit.js
Normal file
786
test/acceptance/user-database-timeout-limit.js
Normal file
@ -0,0 +1,786 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;
|
||||
|
||||
const pointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.3),
|
||||
'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const validationPointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.3),
|
||||
ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const createMapConfig = ({
|
||||
version = '1.6.0',
|
||||
type = 'cartodb',
|
||||
sql = pointSleepSql,
|
||||
cartocss = TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version = '2.3.0',
|
||||
interactivity = 'cartodb_id',
|
||||
countBy = 'cartodb_id',
|
||||
attributes
|
||||
} = {}) => ({
|
||||
version,
|
||||
layers: [{
|
||||
type,
|
||||
options: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
cartocss,
|
||||
cartocss_version,
|
||||
attributes,
|
||||
interactivity
|
||||
}
|
||||
}],
|
||||
analyses: [
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: sql
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
count: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: countBy,
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const DATASOURCE_TIMEOUT_ERROR = {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
};
|
||||
|
||||
describe('user database timeout limit', function () {
|
||||
describe('dataview', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but dataview request fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getDataview('count', params, (err, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('raster', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching raster tiles', function () {
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
describe('with onTileErrorStrategy ENABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = true;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('"png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'png',
|
||||
layers: [ 0 ]
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('"static png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png'
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('"png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'png',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('"static png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('vector', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching vector tiles', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('"mvt" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'mvt',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('interactivity', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql, interactivity: 'val' });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching interactivity tiles', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ interactivity: 'val' });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('"grid.json" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'grid.json',
|
||||
layers: 'mapnik',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('torque', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
type: 'torque',
|
||||
cartocss: TestClient.CARTOCSS.TORQUE
|
||||
});
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'torque-layer0', index: 0, type: 'torque' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching torque tiles', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
type: 'torque',
|
||||
cartocss: TestClient.CARTOCSS.TORQUE
|
||||
});
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('"torque.json" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'torque.json',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('".png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'torque.png',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, attributes) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(attributes, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('attributes:', function () {
|
||||
describe('while validating in map instatiation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
attributes: {
|
||||
id: 'cartodb_id',
|
||||
columns: [ 'val' ]
|
||||
}
|
||||
});
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: {
|
||||
id: 'layer0',
|
||||
index: 0,
|
||||
type: 'mapnik'
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching by feature id', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
attributes: {
|
||||
id: 'cartodb_id',
|
||||
columns: [ 'val' ]
|
||||
}
|
||||
});
|
||||
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
featureId: 1,
|
||||
layer: 0,
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getAttributes(params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
394
test/acceptance/user-render-timeout-limit.js
Normal file
394
test/acceptance/user-render-timeout-limit.js
Normal file
@ -0,0 +1,394 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;
|
||||
|
||||
const pointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.5),
|
||||
'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
// during instatiation we validate tile 30/0/0, creating a point in that tile `pg_sleep` will throw a timeout
|
||||
const validationPointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.5),
|
||||
ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const createMapConfig = ({
|
||||
version = '1.6.0',
|
||||
type = 'cartodb',
|
||||
sql = pointSleepSql,
|
||||
cartocss = TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version = '2.3.0',
|
||||
interactivity = 'cartodb_id',
|
||||
countBy = 'cartodb_id'
|
||||
} = {}) => ({
|
||||
version,
|
||||
layers: [{
|
||||
type,
|
||||
options: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
cartocss,
|
||||
cartocss_version,
|
||||
interactivity
|
||||
}
|
||||
}],
|
||||
analyses: [
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: sql
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
count: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: countBy,
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('user render timeout limit', function () {
|
||||
describe('map instantiation => validation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: ["You are over platform\'s limits. Please contact us to know more details"],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: "You are over platform\'s limits. Please contact us to know more details",
|
||||
layer: {
|
||||
id: "layer0",
|
||||
index: 0,
|
||||
type: "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('raster', function () {
|
||||
describe('with onTileErrorStrategy ENABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = true;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but tile request fails due to render timeout', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, {}, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works and render tile fails', function (done) {
|
||||
var params = {
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: ["You are over platform\'s limits. Please contact us to know more details"],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: "You are over platform\'s limits. Please contact us to know more details"
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('vector', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but vector tile request fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(tile, {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('interativity', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but "grid.json" tile request fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
layers: 'mapnik',
|
||||
format: 'grid.json',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(tile, {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('static images', function () {
|
||||
describe('with onTileErrorStrategy ENABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = true;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but static image fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png'
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works and render tile fails', function (done) {
|
||||
const params = {
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, function (err, res, timeoutError) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: ["You are over platform\'s limits. Please contact us to know more details"],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: "You are over platform\'s limits. Please contact us to know more details"
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -218,6 +218,114 @@ describe('widgets-regressions', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not count the polygons outside the bounding box', function(done) {
|
||||
|
||||
// $ % $ = not intersecting left triangle
|
||||
// $$ **VVVVV** %% % = not intersecting right triangle
|
||||
// $$$ *VVVVV* %%% * = intersecting triangle
|
||||
// $$$$ ***** %%%% V = bounding box
|
||||
// $$$$$ *** %%%%%
|
||||
// $$$$$$ * %%%%%%
|
||||
// $$$$$$$ %%%%%%%
|
||||
// $$$$$$$$ %%%%%%%%
|
||||
|
||||
const notIntersectingLeftTriangle = {
|
||||
type: "Polygon",
|
||||
coordinates:[[
|
||||
[-161.015625,69.28725695167886],
|
||||
[-162.7734375,-7.710991655433217],
|
||||
[-40.78125,-8.059229627200192],
|
||||
[-161.015625,69.28725695167886]
|
||||
]]
|
||||
};
|
||||
|
||||
const notIntersectingRightTriangle = {
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[-29.179687499999996,-7.01366792756663],
|
||||
[103.71093749999999,-6.664607562172573],
|
||||
[105.46875,69.16255790810501],
|
||||
[-29.179687499999996,-7.01366792756663]
|
||||
]]
|
||||
};
|
||||
|
||||
const intersectingTriangle = {
|
||||
type: "Polygon",
|
||||
coordinates:[[
|
||||
[-117.42187500000001,68.13885164925573],
|
||||
[-35.859375,20.96143961409684],
|
||||
[59.4140625,68.52823492039876],
|
||||
[-117.42187500000001,68.13885164925573]
|
||||
]]
|
||||
};
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
ST_TRANSFORM(ST_SETSRID(ST_GeomFromGeoJSON(
|
||||
'${JSON.stringify(notIntersectingLeftTriangle)}'
|
||||
), 4326), 3857) AS the_geom_webmercator, 1 AS cartodb_id, 'notIntersectingLeftTriangle' AS name
|
||||
UNION
|
||||
SELECT
|
||||
ST_TRANSFORM(ST_SETSRID(ST_GeomFromGeoJSON(
|
||||
'${JSON.stringify(notIntersectingRightTriangle)}'
|
||||
), 4326), 3857), 2, 'notIntersectingRightTriangle'
|
||||
UNION
|
||||
SELECT
|
||||
ST_TRANSFORM(ST_SETSRID(ST_GeomFromGeoJSON(
|
||||
'${JSON.stringify(intersectingTriangle)}'
|
||||
), 4326), 3857), 3, 'intersectingTriangle'
|
||||
`;
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
val_formula: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: "name",
|
||||
aggregation: "count",
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": query
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
const params = {
|
||||
bbox: '-77.34374999999999,45.82879925192134,17.578125,55.97379820507658'
|
||||
};
|
||||
this.testClient.getDataview('val_formula', params, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.categories.length, 1);
|
||||
assert.equal(dataview.categories[0].category, 'intersectingTriangle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -3,7 +3,8 @@
|
||||
var qs = require('querystring');
|
||||
var step = require('step');
|
||||
var urlParser = require('url');
|
||||
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
@ -14,13 +15,21 @@ var helper = require('./test_helper');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
serverOptions.analysis.batch.inlineExecution = true;
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
const MAPNIK_SUPPORTED_FORMATS = {
|
||||
'png': true,
|
||||
'png32': true,
|
||||
'grid.json': true,
|
||||
'geojson': true,
|
||||
'mvt': true
|
||||
}
|
||||
|
||||
function TestClient(config, apiKey) {
|
||||
this.mapConfig = isMapConfig(config) ? config : null;
|
||||
this.template = isTemplate(config) ? config : null;
|
||||
this.apiKey = apiKey;
|
||||
this.keysToDelete = {};
|
||||
this.server = new CartodbWindshaft(serverOptions);
|
||||
}
|
||||
|
||||
module.exports = TestClient;
|
||||
@ -72,6 +81,34 @@ module.exports.CARTOCSS = {
|
||||
' line-width: 0.5;',
|
||||
' line-opacity: 1;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
|
||||
TORQUE: [
|
||||
'Map {',
|
||||
' -torque-frame-count: 256;',
|
||||
' -torque-animation-duration: 30;',
|
||||
' -torque-time-attribute: "cartodb_id";',
|
||||
' -torque-aggregation-function: "count(1)";',
|
||||
' -torque-resolution: 4;',
|
||||
' -torque-data-aggregation: linear;',
|
||||
'}',
|
||||
'#layer {',
|
||||
' marker-width: 7;',
|
||||
' marker-fill: #FFB927;',
|
||||
' marker-fill-opacity: 0.9;',
|
||||
' marker-line-width: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-opacity: 1;',
|
||||
' comp-op: lighter;',
|
||||
'}',
|
||||
'#layer[frame-offset=1] {',
|
||||
' marker-width: 9;',
|
||||
' marker-fill-opacity: 0.45;',
|
||||
'}',
|
||||
'#layer[frame-offset=2] {',
|
||||
' marker-width: 11;',
|
||||
' marker-fill-opacity: 0.225;',
|
||||
'}'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
@ -97,7 +134,7 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@ -156,7 +193,7 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
|
||||
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@ -208,7 +245,7 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@ -265,7 +302,7 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
}
|
||||
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '/search?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@ -332,7 +369,7 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@ -385,7 +422,7 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
}
|
||||
url = '/api/v1/map/' + layergroupId + '/dataview/' + dataviewName + '?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@ -404,12 +441,16 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
);
|
||||
},
|
||||
function finish(err, dataview) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (layergroupId) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
|
||||
return callback(err, dataview);
|
||||
return callback(null, dataview);
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -446,7 +487,7 @@ TestClient.prototype.getFeatureAttributes = function(featureId, layerId, params,
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@ -485,7 +526,7 @@ TestClient.prototype.getFeatureAttributes = function(featureId, layerId, params,
|
||||
|
||||
url = '/api/v1/map/' + layergroupId + '/' + layerId + '/attributes/' + featureId;
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@ -529,6 +570,11 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
|
||||
var layergroupId;
|
||||
|
||||
if (params.layergroupid) {
|
||||
layergroupId = params.layergroupid
|
||||
}
|
||||
|
||||
step(
|
||||
function createTemplate () {
|
||||
var next = this;
|
||||
@ -543,7 +589,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
|
||||
params.placeholders = params.placeholders || {};
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: urlNamed + '?' + qs.stringify({ api_key: self.apiKey }),
|
||||
method: 'POST',
|
||||
@ -570,12 +616,16 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
function createLayergroup(err, templateId) {
|
||||
var next = this;
|
||||
|
||||
if (layergroupId) {
|
||||
return next(null, layergroupId);
|
||||
}
|
||||
|
||||
var data = templateId ? params.placeholders : self.mapConfig
|
||||
var path = templateId ?
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
|
||||
url;
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: path,
|
||||
method: 'POST',
|
||||
@ -616,6 +666,10 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
|
||||
var format = params.format || 'png';
|
||||
|
||||
if (layers === undefined && !MAPNIK_SUPPORTED_FORMATS[format]) {
|
||||
throw new Error(`Missing layer filter while fetching ${format} tile, review params argument`);
|
||||
}
|
||||
|
||||
url += [z,x,y].join('/');
|
||||
url += '.' + format;
|
||||
|
||||
@ -631,32 +685,30 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
var expectedResponse = {
|
||||
status: params.status || 200,
|
||||
var expectedResponse = Object.assign({}, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
};
|
||||
}, params.response);
|
||||
|
||||
|
||||
var isPng = format.match(/png$/);
|
||||
|
||||
if (isPng) {
|
||||
request.encoding = 'binary';
|
||||
expectedResponse.headers['Content-Type'] = 'image/png';
|
||||
}
|
||||
|
||||
var isMvt = format.match(/mvt$/);
|
||||
|
||||
if (isMvt) {
|
||||
request.encoding = 'binary';
|
||||
|
||||
if (expectedResponse.status === 200) {
|
||||
expectedResponse.headers['Content-Type'] = 'application/x-protobuf';
|
||||
} else if (expectedResponse.status === 204) {
|
||||
expectedResponse.headers['Content-Type'] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isGeojson = format.match(/geojson$/);
|
||||
|
||||
if (isGeojson) {
|
||||
@ -671,29 +723,38 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
}
|
||||
|
||||
assert.response(server, request, expectedResponse, function(res, err) {
|
||||
if (params.contentType) {
|
||||
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
}
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
var obj;
|
||||
|
||||
if (isPng) {
|
||||
obj = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
else if (isMvt) {
|
||||
if (res.body) {
|
||||
obj = new mapnik.VectorTile(z, x, y);
|
||||
obj.setDataSync(new Buffer(res.body, 'binary'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj = JSON.parse(res.body);
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/x-protobuf':
|
||||
body = new mapnik.VectorTile(z, x, y);
|
||||
body.setDataSync(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, res, obj);
|
||||
next(null, res, body);
|
||||
});
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
if (layergroupId) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
return callback(err, res, image);
|
||||
}
|
||||
);
|
||||
@ -718,7 +779,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@ -743,7 +804,111 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, JSON.parse(res.body));
|
||||
return callback(null, parsedBody);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getStaticCenter = function (params, callback) {
|
||||
var self = this;
|
||||
|
||||
let { layergroupid, z, lat, lng, width, height, format } = params
|
||||
|
||||
var url = `/api/v1/map/`;
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
|
||||
if (layergroupid) {
|
||||
return next(null, layergroupid);
|
||||
}
|
||||
|
||||
var data = self.mapConfig
|
||||
var path = url;
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next(null, JSON.parse(res.body).layergroupid);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getStaticResult(err, _layergroupid) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
layergroupid = _layergroupid;
|
||||
|
||||
url = `/api/v1/map/static/center/${layergroupid}/${z}/${lat}/${lng}/${width}/${height}.${format}`
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
}
|
||||
|
||||
var request = {
|
||||
url: url,
|
||||
encoding: 'binary',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
var expectedResponse = Object.assign({}, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
}, params.response);
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, res, body);
|
||||
});
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
if (layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
return callback(err, res, image);
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -762,7 +927,7 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@ -823,7 +988,7 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server, request, expectedResponse, function(res, err) {
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
next(null, res, JSON.parse(res.body));
|
||||
});
|
||||
@ -836,11 +1001,119 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getAttributes = function(params, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!Number.isFinite(params.featureId)) {
|
||||
throw new Error('featureId param must be a number')
|
||||
}
|
||||
|
||||
if (!Number.isFinite(params.layer)) {
|
||||
throw new Error('layer param must be a number')
|
||||
}
|
||||
|
||||
var url = '/api/v1/map';
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({ api_key: this.apiKey });
|
||||
}
|
||||
|
||||
var layergroupid;
|
||||
|
||||
if (params.layergroupid) {
|
||||
layergroupid = params.layergroupid
|
||||
}
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
|
||||
if (layergroupid) {
|
||||
return next(null, layergroupid);
|
||||
}
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
return next(null, parsedBody.layergroupid);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getAttributes(err, _layergroupid) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
layergroupid = _layergroupid;
|
||||
|
||||
url = `/api/v1/map/${layergroupid}/${params.layer}/attributes/${params.featureId}`;
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
}
|
||||
|
||||
var request = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
var expectedResponse = params.response || {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var attributes = JSON.parse(res.body);
|
||||
|
||||
next(null, res, attributes);
|
||||
});
|
||||
},
|
||||
function finish(err, res, attributes) {
|
||||
if (layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
|
||||
return callback(err, res, attributes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.drain = function(callback) {
|
||||
helper.deleteRedisKeys(this.keysToDelete, callback);
|
||||
};
|
||||
|
||||
module.exports.getStaticMap = function getStaticMap(templateName, params, callback) {
|
||||
var self = this;
|
||||
|
||||
self.server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
if (!callback) {
|
||||
callback = params;
|
||||
params = null;
|
||||
@ -871,9 +1144,56 @@ module.exports.getStaticMap = function getStaticMap(templateName, params, callba
|
||||
// this could be removed once named maps are invalidated, otherwise you hits the cache
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(server, requestOptions, expectedResponse, function (res, err) {
|
||||
assert.response(self.server, requestOptions, expectedResponse, function (res, err) {
|
||||
helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
|
||||
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.setUserRenderTimeoutLimit = function (user, userTimeoutLimit, callback) {
|
||||
const userTimeoutLimitsKey = `limits:timeout:${user}`;
|
||||
const params = [
|
||||
userTimeoutLimitsKey,
|
||||
'render', userTimeoutLimit,
|
||||
'render_public', userTimeoutLimit
|
||||
];
|
||||
|
||||
this.keysToDelete[userTimeoutLimitsKey] = 5;
|
||||
|
||||
helper.configureMetadata('hmset', params, callback);
|
||||
};
|
||||
|
||||
TestClient.prototype.setUserDatabaseTimeoutLimit = function (timeoutLimit, callback) {
|
||||
const dbname = _.template(global.environment.postgres_auth_user, { user_id: 1 }) + '_db';
|
||||
const dbuser = _.template(global.environment.postgres_auth_user, { user_id: 1 })
|
||||
const pass = _.template(global.environment.postgres_auth_pass, { user_id: 1 })
|
||||
const publicuser = global.environment.postgres.user;
|
||||
|
||||
// we need to guarantee all new connections have the new settings
|
||||
helper.cleanPGPoolConnections()
|
||||
|
||||
const psql = new PSQL({
|
||||
user: 'postgres',
|
||||
dbname: dbname,
|
||||
host: global.environment.postgres.host,
|
||||
port: global.environment.postgres.port
|
||||
});
|
||||
|
||||
step(
|
||||
function configureTimeouts () {
|
||||
const timeoutSQLs = [
|
||||
`ALTER ROLE "${publicuser}" SET STATEMENT_TIMEOUT TO ${timeoutLimit}`,
|
||||
`ALTER ROLE "${dbuser}" SET STATEMENT_TIMEOUT TO ${timeoutLimit}`,
|
||||
`ALTER DATABASE "${dbname}" SET STATEMENT_TIMEOUT TO ${timeoutLimit}`
|
||||
];
|
||||
|
||||
const group = this.group();
|
||||
|
||||
timeoutSQLs.forEach(sql => psql.query(sql, group()));
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ var lzmaWorker = new LZMA();
|
||||
var redis = require('redis');
|
||||
var nock = require('nock');
|
||||
var log4js = require('log4js');
|
||||
var pg = require('pg');
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require(__dirname + '/../../config/environments/test');
|
||||
@ -127,6 +128,11 @@ afterEach(function(done) {
|
||||
});
|
||||
});
|
||||
|
||||
function cleanPGPoolConnections () {
|
||||
// TODO: this method will be replaced by psql.end
|
||||
pg.end();
|
||||
}
|
||||
|
||||
function deleteRedisKeys(keysToDelete, callback) {
|
||||
|
||||
if (Object.keys(keysToDelete).length === 0) {
|
||||
@ -166,12 +172,30 @@ function rmdirRecursiveSync(dirname) {
|
||||
}
|
||||
}
|
||||
|
||||
function configureMetadata(action, params, callback) {
|
||||
redisClient.SELECT(5, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
redisClient[action](params, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteRedisKeys: deleteRedisKeys,
|
||||
lzma_compress_to_base64: lzma_compress_to_base64,
|
||||
checkNoCache: checkNoCache,
|
||||
checkSurrogateKey: checkSurrogateKey,
|
||||
checkCache: checkCache,
|
||||
rmdirRecursiveSync: rmdirRecursiveSync
|
||||
rmdirRecursiveSync: rmdirRecursiveSync,
|
||||
configureMetadata,
|
||||
cleanPGPoolConnections
|
||||
};
|
||||
|
||||
|
@ -6,10 +6,13 @@ var LayergroupController = require('../../../../lib/cartodb/controllers/layergro
|
||||
|
||||
describe('tile stats', function() {
|
||||
|
||||
after(function() {
|
||||
global.statsClient = null;
|
||||
beforeEach(function () {
|
||||
this.statsClient = global.statsClient;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.statsClient = this.statsClient;
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
|
||||
var expectedCalls = 2, // it will call increment once for the general error
|
||||
|
Loading…
Reference in New Issue
Block a user