gristlabs_grist-core/app/server/lib/shutdown.js

124 lines
4.1 KiB
JavaScript

/**
* Module for managing graceful shutdown.
*/
var log = require('app/server/lib/log');
var Promise = require('bluebird');
var os = require('os');
var cleanupHandlers = [];
var signalsHandled = {};
/**
* Adds a handler that should be run on shutdown.
* @param {Object} context The context with which to run the method, and which can be used to
* remove cleanup handlers.
* @param {Function} method The method to run. It may return a promise, which will be waited on.
* @param {Number} timeout Timeout in ms, for how long to wait for the returned promise. Required
* because it's no good for a cleanup handler to block the shutdown process indefinitely.
* @param {String} name A title to show in log messages to distinguish one handler from another.
*/
function addCleanupHandler(context, method, timeout = 1000, name = 'unknown') {
cleanupHandlers.push({
context,
method,
timeout,
name
});
}
exports.addCleanupHandler = addCleanupHandler;
/**
* Removes all cleanup handlers with the given context.
* @param {Object} context The context with which once or more cleanup handlers were added.
*/
function removeCleanupHandlers(context) {
// Maybe there should be gutil.removeFilter(func) which does this in-place.
cleanupHandlers = cleanupHandlers.filter(function(handler) {
return handler.context !== context;
});
}
exports.removeCleanupHandlers = removeCleanupHandlers;
var _cleanupHandlersPromise = null;
/**
* Internal helper which runs all cleanup handlers, with the right contexts and timeouts,
* waits for them, and reports and swallows any errors. It returns a promise that should always be
* fulfilled.
*/
function runCleanupHandlers() {
if (!_cleanupHandlersPromise) {
// Switch out cleanupHandlers, to leave an empty array at the end.
var handlers = cleanupHandlers;
cleanupHandlers = [];
_cleanupHandlersPromise = Promise.all(handlers.map(function(handler) {
return Promise.try(handler.method.bind(handler.context)).timeout(handler.timeout)
.catch(function(err) {
log.warn(`Cleanup error for '${handler.name}' handler: ` + err);
});
}));
}
return _cleanupHandlersPromise;
}
/**
* Internal helper to exit on a signal. It runs the cleanup handlers, and then
* exits propagating the same signal code than the one caught.
*/
function signalExit(signal) {
var prog = 'grist[' + process.pid + ']';
log.info("Server %s got signal %s; cleaning up (%d handlers)",
prog, signal, cleanupHandlers.length);
function dup() {
log.info("Server %s ignoring duplicate signal %s", prog, signal);
}
process.on(signal, dup);
return runCleanupHandlers()
.finally(function() {
log.info("Server %s exiting on %s", prog, signal);
process.removeListener(signal, dup);
delete signalsHandled[signal];
// Exit with the expected exit code for being killed by this signal.
// Unlike re-sending the same signal, the explicit exit works even
// in a situation when Grist is the init (pid 1) process in a container
// See https://github.com/gristlabs/grist-core/pull/830 (and #892)
const signalNumber = os.constants.signals[signal];
process.exit(process.pid, 128 + signalNumber);
});
}
/**
* For the given signals, run cleanup handlers (which may be asynchronous) before re-sending the
* signals to the process. This should only be used for signals that normally kill the process.
* E.g. cleanupOnSignals('SIGINT', 'SIGTERM', 'SIGUSR2');
*/
function cleanupOnSignals(varSignalNames) {
for (var i = 0; i < arguments.length; i++) {
var signal = arguments[i];
if (signalsHandled[signal]) { continue; }
signalsHandled[signal] = true;
process.once(signal, signalExit.bind(null, signal));
}
}
exports.cleanupOnSignals = cleanupOnSignals;
/**
* Run cleanup handlers and exit the process with the given exit code (0 if omitted).
*/
function exit(optExitCode) {
var prog = 'grist[' + process.pid + ']';
var code = optExitCode || 0;
log.info("Server %s cleaning up", prog);
return runCleanupHandlers()
.finally(function() {
log.info("Server %s exiting with code %s", prog, code);
process.exit(code);
});
}
exports.exit = exit;