2011-06-13 11:23:02 +08:00
|
|
|
#!/usr/bin/env node
|
2011-06-10 00:34:02 +08:00
|
|
|
|
2018-10-24 21:42:33 +08:00
|
|
|
'use strict';
|
|
|
|
|
2011-06-13 11:23:02 +08:00
|
|
|
/*
|
|
|
|
* SQL API loader
|
|
|
|
* ===============
|
|
|
|
*
|
2015-12-04 00:28:18 +08:00
|
|
|
* node app [environment]
|
2011-06-13 11:23:02 +08:00
|
|
|
*
|
|
|
|
* environments: [development, test, production]
|
|
|
|
*
|
|
|
|
*/
|
2015-05-12 23:33:41 +08:00
|
|
|
var fs = require('fs');
|
|
|
|
var path = require('path');
|
2018-06-08 19:30:54 +08:00
|
|
|
const fqdn = require('@carto/fqdn-sync');
|
2011-06-10 00:34:02 +08:00
|
|
|
|
2016-09-15 06:36:24 +08:00
|
|
|
var argv = require('yargs')
|
|
|
|
.usage('Usage: $0 <environment> [options]')
|
|
|
|
.help('h')
|
|
|
|
.example(
|
2016-09-15 06:40:25 +08:00
|
|
|
'$0 production -c /etc/sql-api/config.js',
|
|
|
|
'start server in production environment with /etc/sql-api/config.js as config file'
|
2016-09-15 06:36:24 +08:00
|
|
|
)
|
|
|
|
.alias('h', 'help')
|
|
|
|
.alias('c', 'config')
|
|
|
|
.nargs('c', 1)
|
|
|
|
.describe('c', 'Load configuration from path')
|
|
|
|
.argv;
|
|
|
|
|
|
|
|
var environmentArg = argv._[0] || process.env.NODE_ENV || 'development';
|
|
|
|
var configurationFile = path.resolve(argv.config || './config/environments/' + environmentArg + '.js');
|
|
|
|
if (!fs.existsSync(configurationFile)) {
|
|
|
|
console.error('Configuration file "%s" does not exist', configurationFile);
|
|
|
|
process.exit(1);
|
2015-05-12 23:33:41 +08:00
|
|
|
}
|
|
|
|
|
2016-09-15 06:36:24 +08:00
|
|
|
global.settings = require(configurationFile);
|
|
|
|
var ENVIRONMENT = argv._[0] || process.env.NODE_ENV || global.settings.environment;
|
2016-09-15 05:10:14 +08:00
|
|
|
process.env.NODE_ENV = ENVIRONMENT;
|
2015-05-12 23:33:41 +08:00
|
|
|
|
|
|
|
var availableEnvironments = ['development', 'production', 'test', 'staging'];
|
2014-03-14 17:48:33 +08:00
|
|
|
|
2011-06-13 11:23:02 +08:00
|
|
|
// sanity check arguments
|
2016-09-15 05:10:14 +08:00
|
|
|
if (availableEnvironments.indexOf(ENVIRONMENT) === -1) {
|
2016-09-15 06:36:24 +08:00
|
|
|
console.error("node app.js [environment]");
|
|
|
|
console.error("Available environments: " + availableEnvironments.join(', '));
|
2011-06-13 11:23:02 +08:00
|
|
|
process.exit(1);
|
2011-06-10 00:34:02 +08:00
|
|
|
}
|
|
|
|
|
2018-06-08 19:30:54 +08:00
|
|
|
global.settings.api_hostname = fqdn.hostname();
|
2014-03-13 18:52:40 +08:00
|
|
|
|
2015-05-12 23:33:41 +08:00
|
|
|
global.log4js = require('log4js');
|
2016-09-15 02:03:05 +08:00
|
|
|
var log4jsConfig = {
|
|
|
|
appenders: [],
|
|
|
|
replaceConsole: true
|
2014-03-13 18:52:40 +08:00
|
|
|
};
|
2014-03-13 19:09:39 +08:00
|
|
|
|
2016-09-15 03:02:56 +08:00
|
|
|
if ( global.settings.log_filename ) {
|
|
|
|
var logFilename = path.resolve(global.settings.log_filename);
|
2016-09-15 02:03:05 +08:00
|
|
|
var logDirectory = path.dirname(logFilename);
|
|
|
|
if (!fs.existsSync(logDirectory)) {
|
|
|
|
console.error("Log filename directory does not exist: " + logDirectory);
|
2014-05-27 17:51:44 +08:00
|
|
|
process.exit(1);
|
|
|
|
}
|
2016-09-15 02:03:05 +08:00
|
|
|
console.log("Logs will be written to " + logFilename);
|
|
|
|
log4jsConfig.appenders.push(
|
|
|
|
{ type: "file", absolute: true, filename: logFilename }
|
2014-05-27 17:51:44 +08:00
|
|
|
);
|
|
|
|
} else {
|
2016-09-15 02:03:05 +08:00
|
|
|
log4jsConfig.appenders.push(
|
2014-05-27 17:51:44 +08:00
|
|
|
{ type: "console", layout: { type:'basic' } }
|
|
|
|
);
|
|
|
|
}
|
2016-09-15 02:03:05 +08:00
|
|
|
global.log4js.configure(log4jsConfig);
|
2015-05-12 23:33:41 +08:00
|
|
|
global.logger = global.log4js.getLogger();
|
2014-03-13 18:52:40 +08:00
|
|
|
|
2015-12-04 00:28:18 +08:00
|
|
|
|
2011-06-13 11:23:02 +08:00
|
|
|
// kick off controller
|
2015-05-12 23:33:41 +08:00
|
|
|
if ( ! global.settings.base_url ) {
|
|
|
|
global.settings.base_url = '/api/*';
|
|
|
|
}
|
2014-03-13 20:50:53 +08:00
|
|
|
|
|
|
|
var version = require("./package").version;
|
|
|
|
|
2017-03-30 22:13:17 +08:00
|
|
|
var StatsClient = require('./app/stats/client');
|
|
|
|
if (global.settings.statsd) {
|
|
|
|
// Perform keyword substitution in statsd
|
|
|
|
if (global.settings.statsd.prefix) {
|
2018-06-08 19:30:54 +08:00
|
|
|
global.settings.statsd.prefix = global.settings.statsd.prefix.replace(/:host/, fqdn.reverse());
|
2017-03-30 22:13:17 +08:00
|
|
|
}
|
|
|
|
}
|
2017-03-30 23:32:22 +08:00
|
|
|
var statsClient = StatsClient.getInstance(global.settings.statsd);
|
2017-03-30 22:13:17 +08:00
|
|
|
|
2017-03-30 23:32:22 +08:00
|
|
|
var server = require('./app/server')(statsClient);
|
2016-09-27 00:09:27 +08:00
|
|
|
var listener = server.listen(global.settings.node_port, global.settings.node_host);
|
|
|
|
listener.on('listening', function() {
|
2017-02-10 17:03:36 +08:00
|
|
|
console.info("Using Node.js %s", process.version);
|
2016-09-15 06:36:24 +08:00
|
|
|
console.info('Using configuration file "%s"', configurationFile);
|
|
|
|
console.log(
|
|
|
|
"CartoDB SQL API %s listening on %s:%s PID=%d (%s)",
|
|
|
|
version, global.settings.node_host, global.settings.node_port, process.pid, ENVIRONMENT
|
|
|
|
);
|
2012-10-10 00:40:17 +08:00
|
|
|
});
|
2014-03-13 19:09:39 +08:00
|
|
|
|
|
|
|
process.on('uncaughtException', function(err) {
|
2015-05-12 23:33:41 +08:00
|
|
|
global.logger.error('Uncaught exception: ' + err.stack);
|
2014-03-13 19:09:39 +08:00
|
|
|
});
|
2014-05-27 17:51:44 +08:00
|
|
|
|
|
|
|
process.on('SIGHUP', function() {
|
2014-12-02 19:25:25 +08:00
|
|
|
global.log4js.clearAndShutdownAppenders(function() {
|
2016-09-15 02:03:05 +08:00
|
|
|
global.log4js.configure(log4jsConfig);
|
2015-05-12 23:33:41 +08:00
|
|
|
global.logger = global.log4js.getLogger();
|
2014-12-02 19:25:25 +08:00
|
|
|
console.log('Log files reloaded');
|
|
|
|
});
|
2016-09-29 21:09:36 +08:00
|
|
|
|
2016-09-30 23:10:52 +08:00
|
|
|
if (server.batch && server.batch.logger) {
|
|
|
|
server.batch.logger.reopenFileStreams();
|
2018-06-19 00:48:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (server.dataIngestionLogger) {
|
|
|
|
server.dataIngestionLogger.reopenFileStreams();
|
2016-09-30 23:10:52 +08:00
|
|
|
}
|
2014-05-27 17:51:44 +08:00
|
|
|
});
|
2016-01-25 23:28:19 +08:00
|
|
|
|
2019-04-05 01:21:09 +08:00
|
|
|
addHandlers({ killTimeout: 45000 });
|
|
|
|
|
|
|
|
function addHandlers({ killTimeout }) {
|
|
|
|
// FIXME: minimize the number of 'uncaughtException' before uncomment the following line
|
|
|
|
// process.on('uncaughtException', exitProcess(listener, logger, killTimeout));
|
2019-04-05 17:55:58 +08:00
|
|
|
process.on('unhandledRejection', exitProcess({ killTimeout }));
|
|
|
|
process.on('SIGINT', exitProcess({ killTimeout }));
|
|
|
|
process.on('SIGTERM', exitProcess({ killTimeout }));
|
2019-04-05 01:21:09 +08:00
|
|
|
}
|
|
|
|
|
2019-04-05 17:55:58 +08:00
|
|
|
function exitProcess ({ killTimeout }) {
|
2019-04-05 01:21:09 +08:00
|
|
|
return function exitProcessFn (signal) {
|
2019-04-05 17:55:58 +08:00
|
|
|
scheduleForcedExit({ killTimeout });
|
2019-04-05 01:21:09 +08:00
|
|
|
|
|
|
|
let code = 0;
|
|
|
|
|
|
|
|
if (!['SIGINT', 'SIGTERM'].includes(signal)) {
|
|
|
|
const err = signal instanceof Error ? signal : new Error(signal);
|
|
|
|
signal = undefined;
|
|
|
|
code = 1;
|
|
|
|
|
|
|
|
global.logger.fatal(err);
|
|
|
|
} else {
|
|
|
|
global.logger.info(`Process has received signal: ${signal}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
listener.close(function () {
|
|
|
|
server.batch.stop(function () {
|
|
|
|
server.batch.drain(function (err) {
|
|
|
|
if (err) {
|
|
|
|
global.logger.error(err);
|
|
|
|
return process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
global.logger.info(`Process is going to exit with code: ${code}`);
|
|
|
|
global.log4js.shutdown(function () {
|
|
|
|
server.batch.logger.end(function () {
|
|
|
|
process.exit(code);
|
|
|
|
});
|
|
|
|
});
|
2019-04-05 00:21:49 +08:00
|
|
|
});
|
2019-04-05 00:15:21 +08:00
|
|
|
});
|
|
|
|
});
|
2019-04-05 01:21:09 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-04-05 17:55:58 +08:00
|
|
|
function scheduleForcedExit ({ killTimeout }) {
|
2019-04-05 01:21:09 +08:00
|
|
|
// Schedule exit if there is still ongoing work to deal with
|
|
|
|
const killTimer = setTimeout(() => {
|
|
|
|
global.logger.info('Process didn\'t close on time. Force exit');
|
|
|
|
process.exit(1);
|
|
|
|
}, killTimeout);
|
|
|
|
|
|
|
|
// Don't keep the process open just for this
|
|
|
|
killTimer.unref();
|
|
|
|
}
|
2017-03-30 22:27:34 +08:00
|
|
|
|
|
|
|
function isGteMinVersion(version, minVersion) {
|
|
|
|
var versionMatch = /[a-z]?([0-9]*)/.exec(version);
|
|
|
|
if (versionMatch) {
|
|
|
|
var majorVersion = parseInt(versionMatch[1], 10);
|
|
|
|
if (Number.isFinite(majorVersion)) {
|
|
|
|
return majorVersion >= minVersion;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-11-15 18:02:28 +08:00
|
|
|
setInterval(function memoryUsageMetrics () {
|
2018-11-15 21:34:36 +08:00
|
|
|
let memoryUsage = process.memoryUsage();
|
|
|
|
|
2018-11-15 18:02:28 +08:00
|
|
|
Object.keys(memoryUsage).forEach(property => {
|
|
|
|
statsClient.gauge(`sqlapi.memory.${property}`, memoryUsage[property]);
|
|
|
|
});
|
|
|
|
}, 5000);
|
|
|
|
|
2018-11-15 20:49:57 +08:00
|
|
|
function getCPUUsage (oldUsage) {
|
|
|
|
let usage;
|
|
|
|
|
|
|
|
if (oldUsage && oldUsage._start) {
|
|
|
|
usage = Object.assign({}, process.cpuUsage(oldUsage._start.cpuUsage));
|
|
|
|
usage.time = Date.now() - oldUsage._start.time;
|
|
|
|
} else {
|
|
|
|
usage = Object.assign({}, process.cpuUsage());
|
|
|
|
usage.time = process.uptime() * 1000; // s to ms
|
|
|
|
}
|
|
|
|
|
|
|
|
usage.percent = (usage.system + usage.user) / (usage.time * 10);
|
|
|
|
|
|
|
|
Object.defineProperty(usage, '_start', {
|
|
|
|
value: {
|
|
|
|
cpuUsage: process.cpuUsage(),
|
|
|
|
time: Date.now()
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return usage;
|
|
|
|
}
|
2018-11-15 18:02:28 +08:00
|
|
|
|
2018-11-15 20:49:57 +08:00
|
|
|
let previousCPUUsage = getCPUUsage();
|
2018-11-15 18:02:28 +08:00
|
|
|
setInterval(function cpuUsageMetrics () {
|
2018-11-15 20:49:57 +08:00
|
|
|
const CPUUsage = getCPUUsage(previousCPUUsage);
|
2018-11-15 18:02:28 +08:00
|
|
|
|
|
|
|
Object.keys(CPUUsage).forEach(property => {
|
|
|
|
statsClient.gauge(`sqlapi.cpu.${property}`, CPUUsage[property]);
|
|
|
|
});
|
|
|
|
|
|
|
|
previousCPUUsage = CPUUsage;
|
|
|
|
}, 5000);
|
|
|
|
|
2017-03-30 22:27:34 +08:00
|
|
|
if (global.gc && isGteMinVersion(process.version, 6)) {
|
|
|
|
var gcInterval = Number.isFinite(global.settings.gc_interval) ?
|
|
|
|
global.settings.gc_interval :
|
|
|
|
10000;
|
|
|
|
|
|
|
|
if (gcInterval > 0) {
|
|
|
|
setInterval(function gcForcedCycle() {
|
|
|
|
global.gc();
|
|
|
|
}, gcInterval);
|
|
|
|
}
|
|
|
|
}
|
2019-01-04 02:09:01 +08:00
|
|
|
|
|
|
|
const gcStats = require('gc-stats')();
|
|
|
|
|
|
|
|
gcStats.on('stats', function ({ pauseMS, gctype }) {
|
2019-01-04 02:26:44 +08:00
|
|
|
statsClient.timing('sqlapi.gc', pauseMS);
|
|
|
|
statsClient.timing(`sqlapi.gctype.${getGCTypeValue(gctype)}`, pauseMS);
|
2019-01-04 02:09:01 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
function getGCTypeValue (type) {
|
|
|
|
// 1: Scavenge (minor GC)
|
|
|
|
// 2: Mark/Sweep/Compact (major GC)
|
|
|
|
// 4: Incremental marking
|
|
|
|
// 8: Weak/Phantom callback processing
|
|
|
|
// 15: All
|
|
|
|
let value;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 1:
|
|
|
|
value = 'Scavenge';
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
value = 'MarkSweepCompact';
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
value = 'IncrementalMarking';
|
|
|
|
break;
|
|
|
|
case 8:
|
|
|
|
value = 'ProcessWeakCallbacks';
|
|
|
|
break;
|
|
|
|
case 15:
|
|
|
|
value = 'All';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
value = 'Unkown';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|