762a240890
- Log system revamp: - Logs to stdout, disabled while testing - Use header `X-Request-Id`, or create a new `uuid` when no present, to identyfy log entries - Be able to set log level from env variable `LOG_LEVEL`, useful while testing: `LOG_LEVEL=info npm test`; even more human-readable: `LOG_LEVEL=info npm t | ./node_modules/.bin/pino-pretty` - Be able to reduce the footprint in the final log file depending on the environment - Use one logger for every service: Queries, Batch Queries (Jobs), and Data Ingestion (CopyTo/CopyFrom) - Stop using headers such as: `X-SQL-API-Log`, `X-SQL-API-Profiler`, and `X-SQL-API-Errors` as a way to log info. - Be able to tag requests with labels as an easier way to provide business metrics - Metro: Add log-collector utility (`metro`), it will be moved to its own repository. Attaching it here fro development purposes. Try it with the following command `LOG_LEVEL=info npm t | node metro` - Metro: Creates `metrics-collector.js` a stream to update Prometheus' counters and histograms and exposes them via Express' app (`:9145/metrics`). Use the ones defined in `grok_exporter` Announcements: - Profiler is always set. No need to check its existence anymore - Unify profiler usage for every endpoint Bug fixes: - Avoid hung requests while fetching user identifier
301 lines
8.5 KiB
JavaScript
301 lines
8.5 KiB
JavaScript
'use strict';
|
|
|
|
require('../helper');
|
|
var assert = require('assert');
|
|
var appServer = require('../../lib/server');
|
|
var redisUtils = require('./redis-utils');
|
|
var debug = require('debug')('batch-test-client');
|
|
|
|
var JobStatus = require('../../lib/batch/job-status');
|
|
var metadataBackend = require('cartodb-redis')({ pool: redisUtils.getPool() });
|
|
var batchFactory = require('../../lib/batch/index');
|
|
var Logger = require('../../lib/utils/logger');
|
|
|
|
function response (code) {
|
|
return {
|
|
status: code
|
|
};
|
|
}
|
|
|
|
var RESPONSE = {
|
|
OK: response(200),
|
|
CREATED: response(201),
|
|
BAD_REQUEST: response(400)
|
|
};
|
|
|
|
function BatchTestClient (config) {
|
|
this.config = config || {};
|
|
this.server = appServer();
|
|
|
|
const logger = new Logger();
|
|
this.batch = batchFactory(metadataBackend, redisUtils.getPool(), this.config.name, undefined, logger);
|
|
this.batch.start();
|
|
|
|
this.pendingJobs = [];
|
|
this.ready = false;
|
|
this.batch.on('ready', function () {
|
|
this.ready = true;
|
|
this.pendingJobs.forEach(function (pendingJob) {
|
|
this.createJob(pendingJob.job, pendingJob.override, pendingJob.callback);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}
|
|
|
|
module.exports = BatchTestClient;
|
|
|
|
BatchTestClient.prototype.isReady = function () {
|
|
return this.ready;
|
|
};
|
|
|
|
BatchTestClient.prototype.getExpectedResponse = function (override) {
|
|
return override.response || this.config.response || RESPONSE.CREATED;
|
|
};
|
|
|
|
BatchTestClient.prototype.createJob = function (job, override, callback) {
|
|
if (!callback) {
|
|
callback = override;
|
|
override = {};
|
|
}
|
|
if (!this.isReady()) {
|
|
this.pendingJobs.push({
|
|
job: job,
|
|
override: override || {},
|
|
callback: callback
|
|
});
|
|
return debug('Waiting for Batch service to be ready');
|
|
}
|
|
assert.response(
|
|
this.server,
|
|
{
|
|
url: this.getUrl(override),
|
|
headers: {
|
|
host: this.getHost(override),
|
|
'Content-Type': 'application/json',
|
|
authorization: this.getAuthorization(override)
|
|
},
|
|
method: 'POST',
|
|
data: JSON.stringify(job)
|
|
},
|
|
this.getExpectedResponse(override),
|
|
function (err, res) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (res.statusCode < 400) {
|
|
return callback(null, new JobResult(JSON.parse(res.body), this, override), res);
|
|
} else {
|
|
return callback(null, res);
|
|
}
|
|
}.bind(this)
|
|
);
|
|
};
|
|
|
|
BatchTestClient.prototype.getJobStatus = function (jobId, override, callback) {
|
|
assert.response(
|
|
this.server,
|
|
{
|
|
url: this.getUrl(override, jobId),
|
|
headers: {
|
|
host: this.getHost(override),
|
|
authorization: this.getAuthorization(override)
|
|
},
|
|
method: 'GET',
|
|
timeout: override.timeout
|
|
},
|
|
RESPONSE.OK,
|
|
function (err, res) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
return callback(null, JSON.parse(res.body));
|
|
}
|
|
);
|
|
};
|
|
|
|
BatchTestClient.prototype.getWorkInProgressJobs = function (override, callback) {
|
|
if (!callback) {
|
|
callback = override;
|
|
override = {};
|
|
}
|
|
|
|
assert.response(
|
|
this.server,
|
|
{
|
|
url: '/api/v1/jobs-wip',
|
|
headers: {
|
|
host: this.getHost(override)
|
|
},
|
|
method: 'GET'
|
|
},
|
|
RESPONSE.OK,
|
|
function (err, res) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
return callback(null, JSON.parse(res.body));
|
|
}
|
|
);
|
|
};
|
|
|
|
BatchTestClient.prototype.cancelJob = function (jobId, override, callback) {
|
|
assert.response(
|
|
this.server,
|
|
{
|
|
url: this.getUrl(override, jobId),
|
|
headers: {
|
|
host: this.getHost(override)
|
|
},
|
|
method: 'DELETE'
|
|
},
|
|
override.statusCode,
|
|
function (err, res) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
return callback(null, JSON.parse(res.body));
|
|
}
|
|
);
|
|
};
|
|
|
|
BatchTestClient.prototype.drain = function (callback) {
|
|
this.batch.stop(function () {
|
|
return redisUtils.clean('batch:*', callback);
|
|
});
|
|
};
|
|
|
|
BatchTestClient.prototype.getHost = function (override) {
|
|
return override.host || this.config.host || 'vizzuality.cartodb.com';
|
|
};
|
|
|
|
BatchTestClient.prototype.getAuthorization = function (override) {
|
|
const auth = override.authorization || this.config.authorization;
|
|
|
|
if (auth) {
|
|
return `Basic ${Buffer.from(auth).toString('base64')}`;
|
|
}
|
|
};
|
|
|
|
BatchTestClient.prototype.getUrl = function (override, jobId) {
|
|
var urlParts = ['/api/v2/sql/job'];
|
|
if (jobId) {
|
|
urlParts.push(jobId);
|
|
}
|
|
return `${urlParts.join('/')}${override.anonymous ? '' : '?api_key=' + this.getApiKey(override)}`;
|
|
};
|
|
|
|
BatchTestClient.prototype.getApiKey = function (override) {
|
|
return override.apiKey || this.config.apiKey || '1234';
|
|
};
|
|
|
|
/** **************** JobResult ******************/
|
|
|
|
function JobResult (job, batchTestClient, override) {
|
|
this.job = job;
|
|
this.batchTestClient = batchTestClient;
|
|
this.override = override;
|
|
}
|
|
|
|
JobResult.prototype.getStatus = function (requiredStatus, callback) {
|
|
if (!callback) {
|
|
callback = requiredStatus;
|
|
requiredStatus = undefined;
|
|
}
|
|
|
|
var self = this;
|
|
var attempts = 1;
|
|
self.override.timeout = 1000;
|
|
|
|
var interval = setInterval(function () {
|
|
self.batchTestClient.getJobStatus(self.job.job_id, self.override, function (err, job) {
|
|
if (err) {
|
|
clearInterval(interval);
|
|
return callback(err);
|
|
}
|
|
attempts += 1;
|
|
|
|
if (attempts > 20) {
|
|
clearInterval(interval);
|
|
return callback(new Error('Reached maximum number of request (20) to check job status'));
|
|
}
|
|
|
|
if (hasRequiredStatus(job, requiredStatus)) {
|
|
clearInterval(interval);
|
|
self.job = job;
|
|
return callback(null, job);
|
|
} else {
|
|
debug('Job %s [status=%s] waiting to be done', self.job.job_id, job.status);
|
|
}
|
|
});
|
|
}, 100);
|
|
};
|
|
|
|
function hasRequiredStatus (job, requiredStatus) {
|
|
if (requiredStatus) {
|
|
return job.status === requiredStatus;
|
|
}
|
|
|
|
if (JobStatus.isFinal(job.status)) {
|
|
if (job.fallback_status !== undefined) {
|
|
if (JobStatus.isFinal(job.fallback_status) || job.fallback_status === JobStatus.SKIPPED) {
|
|
return true;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
JobResult.prototype.cancel = function (callback) {
|
|
var self = this;
|
|
this.override.statusCode = response(RESPONSE.OK);
|
|
this.batchTestClient.cancelJob(this.job.job_id, this.override, function (err, job) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
self.job = job;
|
|
callback(null, job);
|
|
});
|
|
};
|
|
|
|
JobResult.prototype.tryCancel = function (callback) {
|
|
var self = this;
|
|
this.override.statusCode = response();
|
|
this.batchTestClient.cancelJob(this.job.job_id, this.override, function (err, job) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
self.job = job;
|
|
callback(null, job);
|
|
});
|
|
};
|
|
|
|
JobResult.prototype.validateExpectedResponse = function (expected) {
|
|
var actual = this.job.query;
|
|
|
|
actual.query.forEach(function (actualQuery, index) {
|
|
var expectedQuery = expected.query[index];
|
|
assert.ok(expectedQuery);
|
|
Object.keys(expectedQuery).forEach(function (expectedKey) {
|
|
assert.strictEqual(
|
|
actualQuery[expectedKey],
|
|
expectedQuery[expectedKey],
|
|
'Expected value for key "' + expectedKey + '" does not match: ' + actualQuery[expectedKey] + ' ==' +
|
|
expectedQuery[expectedKey] + ' at query index=' + index + '. Full response: ' +
|
|
JSON.stringify(actual, null, 4)
|
|
);
|
|
});
|
|
var propsToCheckDate = ['started_at', 'ended_at'];
|
|
propsToCheckDate.forEach(function (propToCheckDate) {
|
|
if (Object.prototype.hasOwnProperty.call(actualQuery, propToCheckDate)) {
|
|
assert.ok(new Date(actualQuery[propToCheckDate]));
|
|
}
|
|
});
|
|
});
|
|
|
|
assert.strictEqual(actual.onsuccess, expected.onsuccess);
|
|
assert.strictEqual(actual.onerror, expected.onerror);
|
|
};
|