1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00

fix: add option for graceful shutdown (#872)

* fix: add option for graceful shutdown

* fix: gracefulShutdown should close idle keep-alive connections

* fix: eslint import order

* docs: add config options to docs as well
This commit is contained in:
Ivar Conradi Østhus 2021-06-17 20:33:34 +02:00 committed by GitHub
parent d757eeb8cd
commit 4f9deee2ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 84 deletions

View File

@ -99,6 +99,7 @@
"prom-client": "^13.1.0", "prom-client": "^13.1.0",
"response-time": "^2.3.2", "response-time": "^2.3.2",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"stoppable": "^1.1.0",
"unleash-frontend": "4.0.4", "unleash-frontend": "4.0.4",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
@ -110,6 +111,7 @@
"@types/node-fetch": "^2.5.10", "@types/node-fetch": "^2.5.10",
"@types/nodemailer": "^6.4.1", "@types/nodemailer": "^6.4.1",
"@types/owasp-password-strength-test": "^1.3.0", "@types/owasp-password-strength-test": "^1.3.0",
"@types/stoppable": "^1.1.1",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.22.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@ -132,8 +134,8 @@
"superagent": "^6.1.0", "superagent": "^6.1.0",
"supertest": "^6.1.3", "supertest": "^6.1.3",
"ts-jest": "^27.0.0", "ts-jest": "^27.0.0",
"ts-node": "^9.1.1", "ts-node": "^10.0.0",
"tsc-watch": "^4.2.9", "tsc-watch": "^4.4.0",
"typescript": "^4.2.4" "typescript": "^4.2.4"
}, },
"resolutions": { "resolutions": {

View File

@ -90,6 +90,14 @@ const defaultServerOption: IServerOption = {
keepAliveTimeout: 60 * 1000, keepAliveTimeout: 60 * 1000,
headersTimeout: 61 * 1000, headersTimeout: 61 * 1000,
enableRequestLogger: false, enableRequestLogger: false,
gracefulShutdownEnable: safeBoolean(
process.env.GRACEFUL_SHUTDOWN_ENABLE,
true,
),
gracefulShutdownTimeout: safeNumber(
process.env.GRACEFUL_SHUTDOWN_TIMEOUT,
1000,
),
secret: process.env.UNLEASH_SECRET || 'super-secret', secret: process.env.UNLEASH_SECRET || 'super-secret',
}; };

View File

@ -1,10 +1,6 @@
'use strict'; import { knex, Knex } from 'knex';
import { Knex } from 'knex';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
const knex = require('knex');
export function createDb({ export function createDb({
db, db,
getLogger, getLogger,
@ -19,12 +15,13 @@ export function createDb({
asyncStackTraces: true, asyncStackTraces: true,
log: { log: {
debug: msg => logger.debug(msg), debug: msg => logger.debug(msg),
info: msg => logger.info(msg),
warn: msg => logger.warn(msg), warn: msg => logger.warn(msg),
error: msg => logger.error(msg), error: msg => logger.error(msg),
}, },
}); });
} }
// for backward compatibility
module.exports = { module.exports = {
createDb, createDb,
}; };

View File

@ -1,13 +1,14 @@
'use strict'; 'use strict';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { Server } from 'http'; import stoppable, { StoppableServer } from 'stoppable';
import { promisify } from 'util';
import { IUnleash } from './types/core'; import { IUnleash } from './types/core';
import { IUnleashConfig, IUnleashOptions } from './types/option'; import { IUnleashConfig, IUnleashOptions } from './types/option';
import version from './util/version'; import version from './util/version';
import migrator from '../migrator'; import migrator from '../migrator';
import getApp from './app'; import getApp from './app';
import MetricsMonitor, { createMetricsMonitor } from './metrics'; import { createMetricsMonitor } from './metrics';
import { createStores } from './db'; import { createStores } from './db';
import { createServices } from './services'; import { createServices } from './services';
import { createConfig } from './create-config'; import { createConfig } from './create-config';
@ -20,22 +21,11 @@ import { addEventHook } from './event-hook';
import registerGracefulShutdown from './util/graceful-shutdown'; import registerGracefulShutdown from './util/graceful-shutdown';
import { IUnleashStores } from './types/stores'; import { IUnleashStores } from './types/stores';
async function closeServer(
server: Server,
metricsMonitor: MetricsMonitor,
): Promise<void> {
metricsMonitor.stopMonitoring();
return new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()));
});
}
async function destroyDatabase(stores: IUnleashStores): Promise<void> { async function destroyDatabase(stores: IUnleashStores): Promise<void> {
const { db, clientInstanceStore, clientMetricsStore } = stores; const { db, clientInstanceStore, clientMetricsStore } = stores;
clientInstanceStore.destroy(); clientInstanceStore.destroy();
clientMetricsStore.destroy(); clientMetricsStore.destroy();
return db.destroy(); await db.destroy();
} }
async function createApp( async function createApp(
@ -48,6 +38,17 @@ async function createApp(
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
const stores = createStores(config, eventBus); const stores = createStores(config, eventBus);
const services = createServices(stores, config); const services = createServices(stores, config);
const metricsMonitor = createMetricsMonitor();
const stopUnleash = async (server?: StoppableServer) => {
logger.info('Shutting down Unleash...');
if (server) {
const stopServer = promisify(server.stop);
await stopServer();
}
metricsMonitor.stopMonitoring();
await destroyDatabase(stores);
};
if (!config.server.secret) { if (!config.server.secret) {
const secret = await stores.settingStore.get('unleash.secret'); const secret = await stores.settingStore.get('unleash.secret');
@ -55,7 +56,7 @@ async function createApp(
config.server.secret = secret; config.server.secret = secret;
} }
const app = getApp(config, stores, services, eventBus); const app = getApp(config, stores, services, eventBus);
const metricsMonitor = createMetricsMonitor();
if (typeof config.eventHook === 'function') { if (typeof config.eventHook === 'function') {
addEventHook(config.eventHook, stores.eventStore); addEventHook(config.eventHook, stores.eventStore);
} }
@ -80,30 +81,25 @@ async function createApp(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (startApp) { if (startApp) {
const server = app.listen(config.listen, () => const server = stoppable(
app.listen(config.listen, () =>
logger.info('Unleash has started.', server.address()), logger.info('Unleash has started.', server.address()),
),
config.server.gracefulShutdownTimeout,
); );
const stop = async () => {
logger.info('Shutting down Unleash...');
await closeServer(server, metricsMonitor);
return destroyDatabase(stores);
};
server.keepAliveTimeout = config.server.keepAliveTimeout; server.keepAliveTimeout = config.server.keepAliveTimeout;
server.headersTimeout = config.server.headersTimeout; server.headersTimeout = config.server.headersTimeout;
server.on('listening', () => { server.on('listening', () => {
resolve({ ...unleash, server, stop }); resolve({
...unleash,
server,
stop: () => stopUnleash(server),
});
}); });
server.on('error', reject); server.on('error', reject);
} else { } else {
const stop = async () => { resolve({ ...unleash, stop: stopUnleash });
logger.info('Shutting down Unleash...');
metricsMonitor.stopMonitoring();
return destroyDatabase(stores);
};
resolve({ ...unleash, stop });
} }
}); });
} }
@ -125,8 +121,9 @@ async function start(opts: IUnleashOptions = {}): Promise<IUnleash> {
} }
const unleash = await createApp(config, true); const unleash = await createApp(config, true);
logger.info('register graceful shutdown'); if (config.server.gracefulShutdownEnable) {
registerGracefulShutdown(unleash, logger); registerGracefulShutdown(unleash, logger);
}
return unleash; return unleash;
} }

View File

@ -68,6 +68,8 @@ export interface IServerOption {
unleashUrl: string; unleashUrl: string;
serverMetrics: boolean; serverMetrics: boolean;
enableRequestLogger: boolean; enableRequestLogger: boolean;
gracefulShutdownEnable: boolean;
gracefulShutdownTimeout: number;
secret: string; secret: string;
} }

View File

@ -2,27 +2,23 @@ import { Logger } from '../logger';
import { IUnleash } from '../types/core'; import { IUnleash } from '../types/core';
function registerGracefulShutdown(unleash: IUnleash, logger: Logger): void { function registerGracefulShutdown(unleash: IUnleash, logger: Logger): void {
process.on('SIGINT', async () => { const unleashCloser = (signal: string) => async () => {
try { try {
logger.info('Graceful shutdown signal (SIGINT) received.'); logger.info(`Graceful shutdown signal (${signal}) received.`);
await unleash.stop(); await unleash.stop();
logger.info('Unleash has been successfully stopped.');
process.exit(0); process.exit(0);
} catch (e) { } catch (e) {
logger.error('Unable to shutdown Unleash. Hard exit!', e); logger.error('Unable to shutdown Unleash. Hard exit!');
process.exit(1); process.exit(1);
} }
}); };
process.on('SIGTERM', async () => { logger.info('Registering graceful shutdown');
try {
logger.info('Graceful shutdown signal (SIGTERM) received.'); process.on('SIGINT', unleashCloser('SIGINT'));
await unleash.stop(); process.on('SIGHUP', unleashCloser('SIGHUP'));
process.exit(0); process.on('SIGTERM', unleashCloser('SIGTERM'));
} catch (e) {
logger.error('Unable to shutdown Unleash. Hard exit!', e);
process.exit(1);
}
});
} }
export default registerGracefulShutdown; export default registerGracefulShutdown;

View File

@ -4,7 +4,9 @@ import unleash from './lib/server-impl';
import { createConfig } from './lib/create-config'; import { createConfig } from './lib/create-config';
import { LogLevel } from './lib/logger'; import { LogLevel } from './lib/logger';
unleash.start( process.nextTick(async () => {
try {
await unleash.start(
createConfig({ createConfig({
db: { db: {
user: 'unleash_user', user: 'unleash_user',
@ -17,6 +19,8 @@ unleash.start(
server: { server: {
enableRequestLogger: true, enableRequestLogger: true,
baseUriPath: '', baseUriPath: '',
// keepAliveTimeout: 1,
gracefulShutdownEnable: true,
}, },
logLevel: LogLevel.debug, logLevel: LogLevel.debug,
enableOAS: true, enableOAS: true,
@ -25,3 +29,14 @@ unleash.start(
}, },
}), }),
); );
} catch (error) {
if (error.code === 'EADDRINUSE') {
// eslint-disable-next-line no-console
console.warn('Port in use. You might want to reload once more.');
} else {
// eslint-disable-next-line no-console
console.error(error);
process.exit();
}
}
}, 0);

View File

@ -88,6 +88,8 @@ unleash.start(unleashOptions);
- _serverMetrics_ (boolean) - use this option to turn on/off prometheus metrics. - _serverMetrics_ (boolean) - use this option to turn on/off prometheus metrics.
- _baseUriPath_ (string) - use to register a base path for all routes on the application. For example `/my/unleash/base` (note the starting /). Defaults to `/`. Can also be configured through the environment variable `BASE_URI_PATH`. - _baseUriPath_ (string) - use to register a base path for all routes on the application. For example `/my/unleash/base` (note the starting /). Defaults to `/`. Can also be configured through the environment variable `BASE_URI_PATH`.
- _unleashUrl_ (string) - Used to specify the official URL this instance of Unleash can be accessed at for an end user. Can also be configured through the environment variable `UNLEASH_URL`. - _unleashUrl_ (string) - Used to specify the official URL this instance of Unleash can be accessed at for an end user. Can also be configured through the environment variable `UNLEASH_URL`.
- \_gracefulShutdownEnable: (boolean) - Used to control if Unleash should shutdown gracefully (close connections, stop tasks,). Defaults to true. `GRACEFUL_SHUTDOWN_ENABLE`
- \_gracefulShutdownTimeout: (number) - Used to control the timeout, in milliseconds, for shutdown Unleash gracefully. Will kill all connections regardless if this timeout is exceeded. Defaults to 1000ms `GRACEFUL_SHUTDOWN_TIMEOUT`
- **preHook** (function) - this is a hook if you need to provide any middlewares to express before `unleash` adds any. Express app instance is injected as first argument. - **preHook** (function) - this is a hook if you need to provide any middlewares to express before `unleash` adds any. Express app instance is injected as first argument.
- **preRouterHook** (function) - use this to register custom express middlewares before the `unleash` specific routers are added. - **preRouterHook** (function) - use this to register custom express middlewares before the `unleash` specific routers are added.
- **authentication** - (object) - An object for configuring/implementing custom admin authentication - **authentication** - (object) - An object for configuring/implementing custom admin authentication

View File

@ -582,6 +582,26 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
"@tsconfig/node12@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.8.tgz#a883d62f049a64fea1e56a6bbe66828d11c6241b"
integrity sha512-LM6XwBhjZRls1qJGpiM/It09SntEwe9M0riXRfQ9s6XlJQG0JPGl92ET18LtGeYh/GuOtafIXqwZeqLOd0FNFQ==
"@tsconfig/node14@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1"
integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
version "7.1.14" version "7.1.14"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402"
@ -763,6 +783,13 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
"@types/stoppable@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/stoppable/-/stoppable-1.1.1.tgz#a6f1f280e29f8f3c743277534425e0a75041d2f9"
integrity sha512-b8N+fCADRIYYrGZOcmOR8ZNBOqhktWTB/bMUl5LvGtT201QKJZOOH5UsFyI3qtteM6ZAJbJqZoBcLqqxKIwjhw==
dependencies:
"@types/node" "*"
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "20.2.0" version "20.2.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
@ -6222,6 +6249,11 @@ static-extend@^0.1.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
stoppable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
stream-combiner@~0.0.4: stream-combiner@~0.0.4:
version "0.0.4" version "0.0.4"
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
@ -6640,11 +6672,15 @@ ts-jest@^27.0.0:
semver "7.x" semver "7.x"
yargs-parser "20.x" yargs-parser "20.x"
ts-node@^9.1.1: ts-node@^10.0.0:
version "9.1.1" version "10.0.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.0.0.tgz#05f10b9a716b0b624129ad44f0ea05dac84ba3be"
integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== integrity sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg==
dependencies: dependencies:
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.1"
arg "^4.1.0" arg "^4.1.0"
create-require "^1.1.0" create-require "^1.1.0"
diff "^4.0.1" diff "^4.0.1"
@ -6652,10 +6688,10 @@ ts-node@^9.1.1:
source-map-support "^0.5.17" source-map-support "^0.5.17"
yn "3.1.1" yn "3.1.1"
tsc-watch@^4.2.9: tsc-watch@^4.4.0:
version "4.2.9" version "4.4.0"
resolved "https://registry.yarnpkg.com/tsc-watch/-/tsc-watch-4.2.9.tgz#d93fc74233ca4ef7ee6b12d08c0fe6aca3e19044" resolved "https://registry.yarnpkg.com/tsc-watch/-/tsc-watch-4.4.0.tgz#3ebbf1db54bcef6bfe534b330fa87284a4139320"
integrity sha512-DlTaoDs74+KUpyWr7dCGhuscAUKCz6CiFduBN7R9RbLJSSN1moWdwoCLASE7+zLgGvV5AwXfYDiEMAsPGaO+Vw== integrity sha512-+0Yey6ptOOXAnt44OKTk2/EnQnmA0auL7VWXm9d9abMS4tabt0Xdr9B4AK6OJbWAre9ZdLA81+Nk8sz9unptyA==
dependencies: dependencies:
cross-spawn "^7.0.3" cross-spawn "^7.0.3"
node-cleanup "^2.1.2" node-cleanup "^2.1.2"