一段时间以来,我一直在寻找一种干净而健壮的方式来优雅地关闭 nodejs express 服务器。我为此使用了 terminus.js,并提出了以下代码:
#! /usr/bin/env node
const {once} = require('ramda');
const http = require('http');
const {createLogger} = require('./util/logger.js');
const loadConfig = require('./load-config.js');
const {createTerminus} = require('@godaddy/terminus');
const createApp = require('./app');
const config = loadConfig();
const apiPort = config?.apiServer?.port || 3335;
const logger = createLogger(config.logging);
const expressApp = createApp({ config, logger });
const server = http.createServer(expressApp);
const uncaughtHandler = once(async error => {
logger.info('Uncaught exception, attempting graceful shutdown...', { error });
try {
await shutdown();
logger.info('Shutdown completed. Exit cleanly.');
} catch(error) {
logger.error(error);
logger.error('Shutdown timedout or failed. Exit abruptly.');
} finally {
process.exit(1);
}
});
process.on('unhandledRejection', error => { throw error }); // Forward as uncaughtExeption
process.on('uncaughtException', uncaughtHandler);
const terminusConfiguration = Object.freeze({
timeout: 20000,
signals: ['SIGINT', 'SIGTERM', 'SIGQUIT'],
onSignal: async () => await shutdown(),
onShutdown: () => logger.info('Cleanup finished. Exitting'),
logger: logger.error,
});
const shutdown = once(function() {
return new Promise(async (resolve, reject) => {
// After 10 seconds the cleanup times out.
setTimeout(reject, 10000).unref();
await cleanup();
resolve();
});
});
async function cleanup() {
return new Promise(async (resolve, reject) => {
// Note depending on whether the shutdown was initiated by terminus
// or the by the uncaughtHandler, we might still need to close
// the server ourselves.
if(server && server.listening) await closeServer(server);
// simulate other cleanup tasks.
setTimeout(resolve, 3000);
});
}
async function closeServer(server) {
return new Promise(resolve => server.close(resolve));
}
createTerminus(server, terminusConfiguration);
server.listen(apiPort, () => logger.info(`API started on port ${apiPort}`));
Terminus 通过关闭 http 服务器然后调用 onSignal() 来处理信号,然后调用关闭函数。因为我希望在有未捕获异常的情况下重新使用关闭功能,所以关闭功能也将尝试关闭 http 服务器(如果它正在侦听)。
我使用once
from ramda 来确保关闭函数只执行一次,并且随后对它的任何调用都将被忽略。这在首先有一个 SIGINT 信号并且在服务器关闭时有一个 uncaughtException 的情况下效果很好,因为后者将被忽略。但是,当我们首先遇到触发关闭功能的 uncaughtException 并且在我们关闭时有一个信号时,就会出现问题。这将使总站尝试关闭此时可能已经关闭的服务器,从而导致ERR_SERVER_NOT_RUNNING
错误。
有关改进此代码的任何提示?难道没有什么方法可以让 terminus 也处理 uncaughtExeptions 吗?