Skip to content

Commit

Permalink
feat: Internalise graceful shutdown handling
Browse files Browse the repository at this point in the history
- Use the `onClose` hook as handlers
- Allow other signal listeners to run in parallel
- Add options
  • Loading branch information
franky47 committed Dec 19, 2021
1 parent 5feb0b8 commit 9cf468d
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 51 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -36,7 +36,6 @@
"@sentry/node": "^6.16.1",
"fastify": "^3.25.0",
"fastify-autoload": "^3.9.0",
"fastify-graceful-shutdown": "^3.1.0",
"fastify-plugin": "^3.0.0",
"fastify-sensible": "^3.1.2",
"get-port": "^6.0.0",
Expand Down
83 changes: 83 additions & 0 deletions src/graceful-shutdown.ts
@@ -0,0 +1,83 @@
// Based on `fastify-graceful-shutdown`, with some tweaks:
// - allow external signal handlers to be registered
// - don't use specific handlers, use Fastify's `onClose` hooks.
// - await async onClose hooks
// - add some options

import { FastifyPluginAsync } from 'fastify'
import fp from 'fastify-plugin'

export interface GracefulShutdownOptions {
/**
* A list of signals to listen to and trigger
* a graceful shutdown when received.
*
* Defaults to `["SIGINT", "SIGTERM"]`.
*/
signals?: string[]

/**
* How long to wait (in ms) for the signal handlers
* to resolve before doing a hard exit to kill
* the process with `process.exit()`
*
* Defaults to 10 seconds.
*/
timeoutMs?: number

/**
* The exit code to use when hard-exiting after
* the timeout has expired.
*
* Defaults to 1.
*/
hardExitCode?: number
}

export const defaultGracefulShutdownOptions: Required<GracefulShutdownOptions> =
{
signals: ['SIGINT', 'SIGTERM'],
timeoutMs: 10_000,
hardExitCode: 1
}

const gracefulShutdownPlugin: FastifyPluginAsync<GracefulShutdownOptions> =
async function gracefulShutdownPlugin(fastify, userOptions = {}) {
const logger = fastify.log.child({
plugin: 'fastify-micro:graceful-shutdown'
})

const options = {
...defaultGracefulShutdownOptions,
...userOptions
}

options.signals.forEach(signal => {
process.once(signal, () => {
logger.info({ signal }, 'Received signal')
const timeout = setTimeout(() => {
logger.fatal({ signal }, 'Hard-exiting the process after timeout')
process.exit(options.hardExitCode)
}, options.timeoutMs)
fastify.close().then(
() => {
clearTimeout(timeout)
logger.info({ signal }, 'Process terminated')
process.exit(0)
},
error => {
logger.error(
{ signal, error },
'Process terminated with error on `onClose` hook'
)
process.exit(1)
}
)
})
})
}

export default fp(gracefulShutdownPlugin, {
fastify: '3.x',
name: 'fastify-micro:graceful-shutdown'
})
63 changes: 30 additions & 33 deletions src/index.ts
@@ -1,10 +1,10 @@
import checkEnv from '@47ng/check-env'
import Fastify, { FastifyInstance, FastifyServerOptions } from 'fastify'
import { AutoloadPluginOptions, fastifyAutoload } from 'fastify-autoload'
import gracefulShutdown from 'fastify-graceful-shutdown'
import 'fastify-sensible'
import sensible from 'fastify-sensible'
import underPressurePlugin from 'under-pressure'
import gracefulShutdown, { GracefulShutdownOptions } from './graceful-shutdown'
import { getLoggerOptions, makeReqIdGenerator } from './logger'
import sentry, { SentryOptions } from './sentry'

Expand Down Expand Up @@ -62,6 +62,11 @@ export type Options = FastifyServerOptions & {
*/
underPressure?: underPressurePlugin.UnderPressureOptions

/**
* Add custom options for graceful shutdown
*/
gracefulShutdown?: GracefulShutdownOptions | false

/**
* Add custom options for Sentry
*
Expand Down Expand Up @@ -152,47 +157,39 @@ export function createServer(
options.configure(server)
}

const afterPlugins = server.after(error => {
if (error) {
throw error
}
})

// Registered after plugins to let the health check callback
// monitor external services' health.
if (
process.env.FASTIFY_MICRO_DISABLE_SERVICE_HEALTH_MONITORING !== 'true'
) {
const underPressureOptions = options.underPressure || {}
server
.after(error => {
if (error) {
throw error
afterPlugins.register(underPressurePlugin, {
maxEventLoopDelay: 1000, // 1s
// maxHeapUsedBytes: 100 * (1 << 20), // 100 MiB
// maxRssBytes: 100 * (1 << 20), // 100 MiB
healthCheckInterval: 5000, // 5 seconds
exposeStatusRoute: {
url: '/_health',
routeOpts: {
logLevel: 'warn'
}
})
.register(underPressurePlugin, {
maxEventLoopDelay: 1000, // 1s
// maxHeapUsedBytes: 100 * (1 << 20), // 100 MiB
// maxRssBytes: 100 * (1 << 20), // 100 MiB
healthCheckInterval: 5000, // 5 seconds
exposeStatusRoute: {
url: '/_health',
routeOpts: {
logLevel: 'warn'
}
},
...underPressureOptions
})
},
...underPressureOptions
})
}

// Disable graceful shutdown if signal listeners are already in use
// (eg: using Clinic.js or other kinds of wrapping utilities)
const gracefulSignals = ['SIGINT', 'SIGTERM'].filter(
signal => process.listenerCount(signal) > 0
)

if (gracefulSignals.length === 0 && process.env.NODE_ENV !== 'test') {
server.register(gracefulShutdown)
} else if (process.env.NODE_ENV === 'production') {
server.log.warn({
plugin: 'fastify-graceful-shutdown',
msg: 'Automatic graceful shutdown is disabled',
reason: 'Some signal handlers were already registered',
signals: gracefulSignals
if (options.gracefulShutdown !== false) {
afterPlugins.register(async fastify => {
fastify.register(
gracefulShutdown,
options.gracefulShutdown as GracefulShutdownOptions | undefined
)
})
}

Expand Down
18 changes: 1 addition & 17 deletions yarn.lock
Expand Up @@ -2326,14 +2326,6 @@ fastify-error@^0.3.0:
resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.0.tgz#08866323d521156375a8be7c7fcaf98df946fafb"
integrity sha512-Jm2LMTB5rsJqlS1+cmgqqM9tTs0UrlgYR7TvDT3ZgXsUI5ib1NjQlqZHf+tDK5tVPdFGwyq02wAoJtyYIRSiFA==

fastify-graceful-shutdown@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/fastify-graceful-shutdown/-/fastify-graceful-shutdown-3.1.0.tgz#5620a3ee05a7d4694c3de234b380e445f46db3f8"
integrity sha512-5l3EAxYb99xb3NZkdoZwHgMjmyIlRHZaZNEc/U4CCpgosN2Obh7D3a7wjHfNhRtWfddXYkNuZ+KmUhW5SnAKgA==
dependencies:
fastify-plugin "^3.0.0"
fastparallel "^2.4.0"

fastify-plugin@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca"
Expand Down Expand Up @@ -2377,14 +2369,6 @@ fastify@^3.25.0:
semver "^7.3.2"
tiny-lru "^7.0.0"

fastparallel@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688"
integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ==
dependencies:
reusify "^1.0.4"
xtend "^4.0.2"

fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
Expand Down Expand Up @@ -5298,7 +5282,7 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==

xtend@^4.0.2, xtend@~4.0.1:
xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
Expand Down

0 comments on commit 9cf468d

Please sign in to comment.