Refactor startup/shutdown for tests

* `src/node/server.js` can now be run as a script (for normal
    operation) or imported as a module (for tests).
  * Move shutdown actions to `src/node/server.js` to be close to the
    startup actions.
  * Put startup and shutdown in functions so that tests can call them.
  * Use `await` instead of callbacks.
  * Block until the HTTP server is listening to avoid races during
    test startup.
  * Add a new `shutdown` hook.
  * Use the `shutdown` hook to:
      * close the HTTP server
      * call `end()` on the stats collection to cancel its timers
      * call `terminate()` on the Threads.Pool to stop the workers
  * Exit with exit code 0 (instead of 1) on SIGTERM.
  * Export the HTTP server so that tests can get the HTTP server's
    port via `server.address().port` when `settings.port` is 0.
This commit is contained in:
Richard Hansen 2020-09-21 00:42:29 -04:00 committed by John McLear
parent a4be577ed1
commit a000a93dc6
11 changed files with 171 additions and 151 deletions

View file

@ -21,65 +21,112 @@
* limitations under the License.
*/
const log4js = require('log4js')
, NodeVersion = require('./utils/NodeVersion')
, UpdateCheck = require('./utils/UpdateCheck')
;
const log4js = require('log4js');
log4js.replaceConsole();
/*
* early check for version compatibility before calling
* any modules that require newer versions of NodeJS
*/
const NodeVersion = require('./utils/NodeVersion');
NodeVersion.enforceMinNodeVersion('10.13.0');
/*
* Etherpad 1.8.3 will require at least nodejs 10.13.0.
*/
NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3');
// Check if Etherpad version is up-to-date
UpdateCheck.check();
const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB');
const express = require('./hooks/express');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
const npm = require('npm/lib/npm.js');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins');
const settings = require('./utils/Settings');
const util = require('util');
/*
* start up stats counting system
*/
var stats = require('./stats');
stats.gauge('memoryUsage', function() {
return process.memoryUsage().rss;
});
let started = false;
let stopped = false;
/*
* no use of let or await here because it would cause startup
* to fail completely on very early versions of NodeJS
*/
var npm = require("npm/lib/npm.js");
exports.start = async () => {
if (started) return;
started = true;
if (stopped) throw new Error('restart not supported');
npm.load({}, function() {
var settings = require('./utils/Settings');
var db = require('./db/DB');
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
// Check if Etherpad version is up-to-date
UpdateCheck.check();
db.init()
.then(plugins.update)
.then(function() {
console.info("Installed plugins: " + plugins.formatPluginsWithVersion());
console.debug("Installed parts:\n" + plugins.formatParts());
console.debug("Installed hooks:\n" + plugins.formatHooks());
// start up stats counting system
const stats = require('./stats');
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
// Call loadSettings hook
hooks.aCallAll("loadSettings", { settings: settings });
await util.promisify(npm.load)();
// initalize the http server
hooks.callAll("createServer", {});
})
.catch(function(e) {
console.error("exception thrown: " + e.message);
if (e.stack) {
console.log(e.stack);
}
process.exit(1);
});
});
try {
await db.init();
await plugins.update();
console.info('Installed plugins: ' + plugins.formatPluginsWithVersion());
console.debug('Installed parts:\n' + plugins.formatParts());
console.debug('Installed hooks:\n' + plugins.formatHooks());
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('createServer');
} catch (e) {
console.error('exception thrown: ' + e.message);
if (e.stack) console.log(e.stack);
process.exit(1);
}
process.on('uncaughtException', exports.exit);
/*
* Connect graceful shutdown with sigint and uncaught exception
*
* Until Etherpad 1.7.5, process.on('SIGTERM') and process.on('SIGINT') were
* not hooked up under Windows, because old nodejs versions did not support
* them.
*
* According to nodejs 6.x documentation, it is now safe to do so. This
* allows to gracefully close the DB connection when hitting CTRL+C under
* Windows, for example.
*
* Source: https://nodejs.org/docs/latest-v6.x/api/process.html#process_signal_events
*
* - SIGTERM is not supported on Windows, it can be listened on.
* - SIGINT from the terminal is supported on all platforms, and can usually
* be generated with <Ctrl>+C (though this may be configurable). It is not
* generated when terminal raw mode is enabled.
*/
process.on('SIGINT', exports.exit);
// When running as PID1 (e.g. in docker container) allow graceful shutdown on SIGTERM c.f. #3265.
// Pass undefined to exports.exit because this is not an abnormal termination.
process.on('SIGTERM', () => exports.exit());
// Return the HTTP server to make it easier to write tests.
return express.server;
};
exports.stop = async () => {
if (stopped) return;
stopped = true;
console.log('Stopping Etherpad...');
await new Promise(async (resolve, reject) => {
const id = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
await hooks.aCallAll('shutdown');
clearTimeout(id);
resolve();
});
};
exports.exit = async (err) => {
let exitCode = 0;
if (err) {
exitCode = 1;
console.error(err.stack ? err.stack : err);
}
try {
await exports.stop();
} catch (err) {
exitCode = 1;
console.error(err.stack ? err.stack : err);
}
process.exit(exitCode);
};
if (require.main === module) exports.start();