server: Fix handling of errors during startup and shutdown
Before, an unhandled rejection or uncaught exception during startup would cause `exports.exit()` to wait forever for startup completion. Similarly, an error during shutdown would cause `exports.exit()` to wait forever for shutdown to complete. Now any error during startup or shutdown triggers an immediate exit.pull/4740/head
parent
5999d8cd44
commit
ebdb2798ff
|
@ -59,6 +59,7 @@ const State = {
|
||||||
STOPPED: 5,
|
STOPPED: 5,
|
||||||
EXITING: 6,
|
EXITING: 6,
|
||||||
WAITING_FOR_EXIT: 7,
|
WAITING_FOR_EXIT: 7,
|
||||||
|
STATE_TRANSITION_FAILED: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = State.INITIAL;
|
let state = State.INITIAL;
|
||||||
|
@ -85,13 +86,15 @@ exports.start = async () => {
|
||||||
break;
|
break;
|
||||||
case State.STARTING:
|
case State.STARTING:
|
||||||
await startDoneGate;
|
await startDoneGate;
|
||||||
// fall through
|
// Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED.
|
||||||
|
return await exports.start();
|
||||||
case State.RUNNING:
|
case State.RUNNING:
|
||||||
return express.server;
|
return express.server;
|
||||||
case State.STOPPING:
|
case State.STOPPING:
|
||||||
case State.STOPPED:
|
case State.STOPPED:
|
||||||
case State.EXITING:
|
case State.EXITING:
|
||||||
case State.WAITING_FOR_EXIT:
|
case State.WAITING_FOR_EXIT:
|
||||||
|
case State.STATE_TRANSITION_FAILED:
|
||||||
throw new Error('restart not supported');
|
throw new Error('restart not supported');
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown State: ${state.toString()}`);
|
throw new Error(`unknown State: ${state.toString()}`);
|
||||||
|
@ -99,48 +102,54 @@ exports.start = async () => {
|
||||||
logger.info('Starting Etherpad...');
|
logger.info('Starting Etherpad...');
|
||||||
startDoneGate = new Gate();
|
startDoneGate = new Gate();
|
||||||
state = State.STARTING;
|
state = State.STARTING;
|
||||||
|
try {
|
||||||
|
// Check if Etherpad version is up-to-date
|
||||||
|
UpdateCheck.check();
|
||||||
|
|
||||||
// Check if Etherpad version is up-to-date
|
// start up stats counting system
|
||||||
UpdateCheck.check();
|
const stats = require('./stats');
|
||||||
|
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
||||||
|
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
||||||
|
|
||||||
// start up stats counting system
|
process.on('uncaughtException', (err) => exports.exit(err));
|
||||||
const stats = require('./stats');
|
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
|
||||||
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||||
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
process.on('unhandledRejection', (err) => { throw err; });
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => exports.exit(err));
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||||
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
|
// Forcibly remove other signal listeners to prevent them from terminating node before we are
|
||||||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
|
||||||
process.on('unhandledRejection', (err) => { throw err; });
|
// problematic listener. This means that exports.exit is solely responsible for performing all
|
||||||
|
// necessary cleanup tasks.
|
||||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
for (const listener of process.listeners(signal)) {
|
||||||
// Forcibly remove other signal listeners to prevent them from terminating node before we are
|
removeSignalListener(signal, listener);
|
||||||
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
|
}
|
||||||
// problematic listener. This means that exports.exit is solely responsible for performing all
|
process.on(signal, exports.exit);
|
||||||
// necessary cleanup tasks.
|
// Prevent signal listeners from being added in the future.
|
||||||
for (const listener of process.listeners(signal)) {
|
process.on('newListener', (event, listener) => {
|
||||||
removeSignalListener(signal, listener);
|
if (event !== signal) return;
|
||||||
|
removeSignalListener(signal, listener);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
process.on(signal, exports.exit);
|
|
||||||
// Prevent signal listeners from being added in the future.
|
|
||||||
process.on('newListener', (event, listener) => {
|
|
||||||
if (event !== signal) return;
|
|
||||||
removeSignalListener(signal, listener);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await util.promisify(npm.load)();
|
await util.promisify(npm.load)();
|
||||||
await db.init();
|
await db.init();
|
||||||
await plugins.update();
|
await plugins.update();
|
||||||
const installedPlugins = Object.values(pluginDefs.plugins)
|
const installedPlugins = Object.values(pluginDefs.plugins)
|
||||||
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
|
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
|
||||||
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
|
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
logger.info(`Installed plugins: ${installedPlugins}`);
|
logger.info(`Installed plugins: ${installedPlugins}`);
|
||||||
logger.debug(`Installed parts:\n${plugins.formatParts()}`);
|
logger.debug(`Installed parts:\n${plugins.formatParts()}`);
|
||||||
logger.debug(`Installed hooks:\n${plugins.formatHooks()}`);
|
logger.debug(`Installed hooks:\n${plugins.formatHooks()}`);
|
||||||
await hooks.aCallAll('loadSettings', {settings});
|
await hooks.aCallAll('loadSettings', {settings});
|
||||||
await hooks.aCallAll('createServer');
|
await hooks.aCallAll('createServer');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error occurred while starting Etherpad');
|
||||||
|
state = State.STATE_TRANSITION_FAILED;
|
||||||
|
startDoneGate.resolve();
|
||||||
|
return await exports.exit(err);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Etherpad is running');
|
logger.info('Etherpad is running');
|
||||||
state = State.RUNNING;
|
state = State.RUNNING;
|
||||||
|
@ -166,6 +175,7 @@ exports.stop = async () => {
|
||||||
case State.STOPPED:
|
case State.STOPPED:
|
||||||
case State.EXITING:
|
case State.EXITING:
|
||||||
case State.WAITING_FOR_EXIT:
|
case State.WAITING_FOR_EXIT:
|
||||||
|
case State.STATE_TRANSITION_FAILED:
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown State: ${state.toString()}`);
|
throw new Error(`unknown State: ${state.toString()}`);
|
||||||
|
@ -173,14 +183,21 @@ exports.stop = async () => {
|
||||||
logger.info('Stopping Etherpad...');
|
logger.info('Stopping Etherpad...');
|
||||||
let stopDoneGate = new Gate();
|
let stopDoneGate = new Gate();
|
||||||
state = State.STOPPING;
|
state = State.STOPPING;
|
||||||
let timeout = null;
|
try {
|
||||||
await Promise.race([
|
let timeout = null;
|
||||||
hooks.aCallAll('shutdown'),
|
await Promise.race([
|
||||||
new Promise((resolve, reject) => {
|
hooks.aCallAll('shutdown'),
|
||||||
timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
|
new Promise((resolve, reject) => {
|
||||||
}),
|
timeout = setTimeout(() => reject(new Error('Timed out waiting for shutdown tasks')), 3000);
|
||||||
]);
|
}),
|
||||||
clearTimeout(timeout);
|
]);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error occurred while stopping Etherpad');
|
||||||
|
state = State.STATE_TRANSITION_FAILED;
|
||||||
|
stopDoneGate.resolve();
|
||||||
|
return await exports.exit(err);
|
||||||
|
}
|
||||||
logger.info('Etherpad stopped');
|
logger.info('Etherpad stopped');
|
||||||
state = State.STOPPED;
|
state = State.STOPPED;
|
||||||
stopDoneGate.resolve();
|
stopDoneGate.resolve();
|
||||||
|
@ -214,6 +231,7 @@ exports.exit = async (err = null) => {
|
||||||
return await exports.exit();
|
return await exports.exit();
|
||||||
case State.INITIAL:
|
case State.INITIAL:
|
||||||
case State.STOPPED:
|
case State.STOPPED:
|
||||||
|
case State.STATE_TRANSITION_FAILED:
|
||||||
break;
|
break;
|
||||||
case State.EXITING:
|
case State.EXITING:
|
||||||
await exitGate;
|
await exitGate;
|
||||||
|
|
Loading…
Reference in New Issue