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

fix: Do gracefull shutdown of Unleash on 'SIGINT' & 'SIGTERM' (#870)

Unleash will listen for 'SIGINT' & 'SIGTERM'  and close background tasks and db connections before shutting down. 

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Ivar Conradi Østhus 2021-06-15 12:32:35 +02:00 committed by GitHub
parent c467d59a7e
commit 6c2ec59fa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 33 deletions

View File

@ -1,7 +1,7 @@
import { publicFolder } from 'unleash-frontend'; import { publicFolder } from 'unleash-frontend';
import fs from 'fs'; import fs from 'fs';
import EventEmitter from 'events'; import EventEmitter from 'events';
import express from 'express'; import express, { Application } from 'express';
import cors from 'cors'; import cors from 'cors';
import compression from 'compression'; import compression from 'compression';
import favicon from 'serve-favicon'; import favicon from 'serve-favicon';
@ -30,7 +30,7 @@ export default function getApp(
stores: IUnleashStores, stores: IUnleashStores,
services: IUnleashServices, services: IUnleashServices,
eventBus?: EventEmitter, eventBus?: EventEmitter,
): any { ): Application {
const app = express(); const app = express();
const baseUriPath = config.server.baseUriPath || ''; const baseUriPath = config.server.baseUriPath || '';

View File

@ -39,7 +39,7 @@ jest.mock('./metrics', () => ({
jest.mock('./db', () => ({ jest.mock('./db', () => ({
createStores() { createStores() {
return { return {
db: { destroy: cb => cb() }, db: { destroy: () => undefined },
clientInstanceStore: { destroy: noop }, clientInstanceStore: { destroy: noop },
clientMetricsStore: { destroy: noop, on: noop }, clientMetricsStore: { destroy: noop, on: noop },
eventStore, eventStore,

View File

@ -1,12 +1,13 @@
'use strict'; 'use strict';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { Server } from 'http';
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 { createMetricsMonitor } from './metrics'; import MetricsMonitor, { 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';
@ -16,10 +17,13 @@ import * as permissions from './types/permissions';
import AuthenticationRequired from './types/authentication-required'; import AuthenticationRequired from './types/authentication-required';
import * as eventType from './types/events'; import * as eventType from './types/events';
import { addEventHook } from './event-hook'; import { addEventHook } from './event-hook';
import registerGracefulShutdown from './util/graceful-shutdown';
import { IUnleashStores } from './types/stores';
async function closeServer(opts): Promise<void> { async function closeServer(
const { server, metricsMonitor } = opts; server: Server,
metricsMonitor: MetricsMonitor,
): Promise<void> {
metricsMonitor.stopMonitoring(); metricsMonitor.stopMonitoring();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -27,15 +31,11 @@ async function closeServer(opts): Promise<void> {
}); });
} }
async function destroyDatabase(stores): Promise<void> { async function destroyDatabase(stores: IUnleashStores): Promise<void> {
const { db, clientInstanceStore, clientMetricsStore } = stores; const { db, clientInstanceStore, clientMetricsStore } = stores;
return new Promise((resolve, reject) => {
clientInstanceStore.destroy(); clientInstanceStore.destroy();
clientMetricsStore.destroy(); clientMetricsStore.destroy();
return db.destroy();
db.destroy(error => (error ? reject(error) : resolve()));
});
} }
async function createApp( async function createApp(
@ -83,12 +83,11 @@ async function createApp(
const server = app.listen(config.listen, () => const server = app.listen(config.listen, () =>
logger.info('Unleash has started.', server.address()), logger.info('Unleash has started.', server.address()),
); );
const stop = () => { const stop = async () => {
logger.info('Shutting down Unleash...'); logger.info('Shutting down Unleash...');
return closeServer({ server, metricsMonitor }).then(() => await closeServer(server, metricsMonitor);
destroyDatabase(stores), return destroyDatabase(stores);
);
}; };
server.keepAliveTimeout = config.server.keepAliveTimeout; server.keepAliveTimeout = config.server.keepAliveTimeout;
@ -98,7 +97,7 @@ async function createApp(
}); });
server.on('error', reject); server.on('error', reject);
} else { } else {
const stop = () => { const stop = async () => {
logger.info('Shutting down Unleash...'); logger.info('Shutting down Unleash...');
metricsMonitor.stopMonitoring(); metricsMonitor.stopMonitoring();
return destroyDatabase(stores); return destroyDatabase(stores);
@ -125,7 +124,10 @@ async function start(opts: IUnleashOptions = {}): Promise<IUnleash> {
throw err; throw err;
} }
return createApp(config, true); const unleash = await createApp(config, true);
logger.info('register graceful shutdown');
registerGracefulShutdown(unleash, logger);
return unleash;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types

View File

@ -18,6 +18,6 @@ export interface IUnleash {
stores: IUnleashStores; stores: IUnleashStores;
server?: http.Server | https.Server; server?: http.Server | https.Server;
services: IUnleashServices; services: IUnleashServices;
stop: () => void; stop: () => Promise<void>;
version: string; version: string;
} }

View File

@ -0,0 +1,28 @@
import { Logger } from '../logger';
import { IUnleash } from '../types/core';
function registerGracefulShutdown(unleash: IUnleash, logger: Logger): void {
process.on('SIGINT', async () => {
try {
logger.info('Graceful shutdown signal (SIGINT) received.');
await unleash.stop();
process.exit(0);
} catch (e) {
logger.error('Unable to shutdown Unleash. Hard exit!', e);
process.exit(1);
}
});
process.on('SIGTERM', async () => {
try {
logger.info('Graceful shutdown signal (SIGTERM) received.');
await unleash.stop();
process.exit(0);
} catch (e) {
logger.error('Unable to shutdown Unleash. Hard exit!', e);
process.exit(1);
}
});
}
export default registerGracefulShutdown;

View File

@ -5,9 +5,9 @@ title: Configuring Unleash
> This is the guide on how to configure **Unleash v4 self-hosted**. If you are still using Unleash v3 you should checkout [configuring Unleash v3](./configuring_unleash_v3) > This is the guide on how to configure **Unleash v4 self-hosted**. If you are still using Unleash v3 you should checkout [configuring Unleash v3](./configuring_unleash_v3)
# Must configure ## Must configure
## Database details {#database-details} ### Database details {#database-details}
In order for Unleash server to work, you must setup database connection details. In order for Unleash server to work, you must setup database connection details.
@ -22,7 +22,7 @@ In order for Unleash server to work, you must setup database connection details.
- We also support `DATABASE_URL` see [libpq's doc](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for full format explanation. In short: `postgres://USER:PASSWORD@HOST:PORT/DATABASE` - We also support `DATABASE_URL` see [libpq's doc](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for full format explanation. In short: `postgres://USER:PASSWORD@HOST:PORT/DATABASE`
- If you're using secret files from kubernetes and would like to load a `DATABASE_URL` format from a file, use `DATABASE_URL_FILE` and point it to a path containing a connection URL. - If you're using secret files from kubernetes and would like to load a `DATABASE_URL` format from a file, use `DATABASE_URL_FILE` and point it to a path containing a connection URL.
# Nice to configure ## Nice to configure
### Unleash URL {#unleash-url} ### Unleash URL {#unleash-url}
@ -38,7 +38,7 @@ Used to send reset-password mails and welcome mails when onboarding new users. <
For [more details, see here](./email.md) For [more details, see here](./email.md)
# Further customization ## Further customization
In order to customize "anything" in Unleash you need to use [Unleash from Node.js](./getting_started#option-two---from-nodejs) or start the [docker image](./getting_started#option-one---use-docker) with environment variables. In order to customize "anything" in Unleash you need to use [Unleash from Node.js](./getting_started#option-two---from-nodejs) or start the [docker image](./getting_started#option-one---use-docker) with environment variables.
@ -120,18 +120,44 @@ unleash.start(unleashOptions);
If you're using Unleash as part of a larger express app, you can disable the automatic server start by calling `server.create`. It takes the same options as `server.start`, but will not begin listening for connections. If you're using Unleash as part of a larger express app, you can disable the automatic server start by calling `server.create`. It takes the same options as `server.start`, but will not begin listening for connections.
```js ```js
const unleash = require('express');
const unleash = require('unleash-server'); const unleash = require('unleash-server');
// ... const app = express(); const app = express();
unleash const start = async () => {
.create({ const instance = await unleash.create({
databaseUrl: 'postgres://unleash_user:password@localhost:5432/unleash',
});
app.use(instance.app);
console.log(`Unleash app generated and attached to parent application`);
};
start();
```
### Graceful shutdown {#shutdown-unleash}
> PS! Unleash will listen for the `SIGINT` signal and automatically trigger graceful shutdown of Unleash.
If you need to stop Unleash (close database connections, and stop running Unleash tasks) you may use the stop function. Be aware that it is not possible to restart the Unleash instance after stopping it, but you can create a new instance of Unleash.
```js
const express = require('express');
const unleash = require('unleash-server');
const app = express();
const start = async () => {
const instance = await unleash.start({
databaseUrl: 'postgres://unleash_user:password@localhost:5432/unleash', databaseUrl: 'postgres://unleash_user:password@localhost:5432/unleash',
port: 4242, port: 4242,
})
.then(result => {
app.use(result.app);
console.log(`Unleash app generated and attached to parent application`);
}); });
//Sometime later
await instance.stop();
console.log('Unleash has now stopped');
};
start();
``` ```
## Securing Unleash {#securing-unleash} ## Securing Unleash {#securing-unleash}