Skip to content

Commit

Permalink
feat!: terminate with original signal unless useExit0 option passed
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Process will now exit with the signal it received, instead of 0/1.

I believe it is more expected for a process to exit with the signal it was passed, rather than exiting with a generic 0 or 1. There are cases where a 0 exit code is wanted, so I added an option to allow for this behavior to continue. Note that I have inverted the default from what it previously was, thus this should be considered a breaking change.
  • Loading branch information
rexxars committed Aug 9, 2024
1 parent 1e8d8e4 commit cac9c26
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 13 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ npm install --save fastify-graceful-shutdown
fastify.register(require('fastify-graceful-shutdown'))
```

## Passing options

- `timeout` (number) - The timeout in milliseconds to wait before forceful shutdown. Default is 10 seconds.
- `useExit0` (boolean) - Exit with code 0 after successful shutdown, or 1 if reaching the timeout. Otherwise will exit with the received signal, eg `SIGTERM` or `SIGINT`. Default is `false`.
- `resetHandlersOnInit` (boolean) - Remove preexisting listeners if already created by previous instance of same plugin. Default is `false`.

```js
fastify.register(require('fastify-graceful-shutdown'), {
timeout: 5000,
useExit0: true,
resetHandlersOnInit: true,
})
```

## Usage

```js
Expand Down
11 changes: 8 additions & 3 deletions example.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ const fastify = require('fastify')({
},
})

const port = parseInt(process.env.GRACEFUL_SHUTDOWN_PORT, 10) || 3000
const shutdownDelay = parseInt(process.env.GRACEFUL_SHUTDOWN_DELAY, 10) || 3000
const timeout = parseInt(process.env.GRACEFUL_SHUTDOWN_TIMEOUT, 10) || 10000
const useExit0 = process.env.GRACEFUL_SHUTDOWN_USE_EXIT_0 === 'true'

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

fastify.register(require('./')).after((err) => {
fastify.register(require('./'), { useExit0, timeout }).after((err) => {
if (err) {
fastify.log.error(err)
}
// Register custom clean up handler
fastify.gracefulShutdown(async (signal) => {
fastify.log?.info('received signal to shutdown: %s', signal)
await wait(3000)
await wait(shutdownDelay)
fastify.log?.info('graceful shutdown complete')
})
})
Expand Down Expand Up @@ -43,7 +48,7 @@ fastify.get('/', schema, async function (req, reply) {
fastify.listen(
{
host: '127.0.0.1',
port: 3000,
port,
},
(err) => {
if (err) throw err
Expand Down
10 changes: 6 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ type FastifyGracefulShutdownPlugin =

declare module 'fastify' {
interface FastifyInstance {
gracefulShutdown(
handler: (signal: string) => Promise<void> | void,
): void
gracefulShutdown(handler: (signal: string) => Promise<void> | void): void
}
}

declare namespace fastifyGracefulShutdown {
export type fastifyGracefulShutdownOptions = {
timeout?: number
resetHandlersOnInit?: boolean
handlerEventListener?: EventEmitter & { exit(code?: number): never }
useExit0?: boolean
handlerEventListener?: EventEmitter & {
exit(code?: number): never
kill(pid: number, signal?: string | number): boolean
}
}

export const fastifyGracefulShutdown: FastifyGracefulShutdownPlugin
Expand Down
27 changes: 22 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ function fastifyGracefulShutdown(fastify, opts, next) {
const timeout = opts.timeout || 10000
const signals = ['SIGINT', 'SIGTERM']
const handlerEventListener = opts.handlerEventListener || process
const useExit0 = opts.useExit0 || false

// Remove preexisting listeners if already created by previous instance of same plugin
if (opts.resetHandlersOnInit) {
registeredListeners.forEach(({ signal, listener }) => {
handlerEventListener.removeListener(signal, listener)
})
unregisterListeners()
registeredListeners = []
}

Expand All @@ -42,7 +41,13 @@ function fastifyGracefulShutdown(fastify, opts, next) {
}

logger.flush?.()
handlerEventListener.exit(err ? 1 : 0)
if (useExit0) {
handlerEventListener.exit(err ? 1 : 0)
} else {
// Prevent us from catching our own signal
unregisterListeners()
handlerEventListener.kill(process.pid, signal)
}
}

function terminateAfterTimeout(signal, timeout) {
Expand All @@ -51,7 +56,13 @@ function fastifyGracefulShutdown(fastify, opts, next) {
{ signal: signal, timeout: timeout },
'Terminate process after timeout',
)
handlerEventListener.exit(1)
if (useExit0) {
handlerEventListener.exit(1)
} else {
// Prevent us from catching our own signal
unregisterListeners()
handlerEventListener.kill(process.pid, signal)
}
}, timeout).unref()
}

Expand All @@ -72,6 +83,12 @@ function fastifyGracefulShutdown(fastify, opts, next) {
handlers.push(handler)
}

function unregisterListeners() {
registeredListeners.forEach(({ signal, listener }) => {
handlerEventListener.removeListener(signal, listener)
})
}

fastify.decorate('gracefulShutdown', addHandler)

// register handlers
Expand Down
106 changes: 105 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use strict'

const path = require('path')
const childProcess = require('child_process')
const { expect } = require('chai')
const Fastify = require('fastify')

const fastifyGracefulShutdown = require('./')
const { expect } = require('chai')

describe('fastify-graceful-shutdown', () => {
it('can start and stop multiple instances of fastify', async () => {
Expand Down Expand Up @@ -65,6 +68,107 @@ describe('fastify-graceful-shutdown', () => {
expect(removedListeners.length).to.eq(0)
})

// Test all default signals with default configuration (exits with received signal)
;['SIGINT', 'SIGTERM'].forEach((killSignal, i) => {
;['50', '300'].forEach((timeout) => {
it(`exits with passed signal (${killSignal}) - timeout ${timeout}`, function (done) {
this.timeout(10000)

const shouldTimeout = timeout === '50'
const proc = childProcess.fork(path.join(__dirname, 'example.js'), {
stdio: 'pipe',
env: {
...process.env,
GRACEFUL_SHUTDOWN_PORT: `${51093 + i}`,
GRACEFUL_SHUTDOWN_DELAY: '100',
GRACEFUL_SHUTDOWN_TIMEOUT: timeout,
},
})

const logLines = []
proc.on('exit', (_, signal) => {
expect(signal).to.eq(killSignal)
const hasSeenShutdownTimeout = logLines.some((line) =>
line.includes('Terminate process after timeout'),
)
const hasSeenGracefulShutdown = logLines.some((line) =>
line.includes('graceful shutdown complete'),
)
expect(
hasSeenGracefulShutdown,
'seen graceful shutdown log message',
).to.eq(!shouldTimeout)
expect(
hasSeenShutdownTimeout,
'seen shutdown timeout log message',
).to.eq(shouldTimeout)
done()
})

// Send kill signal on first data chunk
proc.stdout.once('data', () =>
expect(proc.kill(killSignal), `${killSignal} success`).to.eq(true),
)

// See if we've seen the appropriate log message
proc.stdout.on('data', (chunk) => {
logLines.push(chunk.toString().trim())
})
})
})
})

// Test all default signals with `useExit0` option (exits with 0/1)
;['SIGINT', 'SIGTERM'].forEach((killSignal, i) => {
;['50', '300'].forEach((timeout) => {
it(`exits with 0 on 'useExit0: true' option (${killSignal}) - timeout ${timeout}`, function (done) {
this.timeout(10000)

const shouldTimeout = timeout === '50'
const proc = childProcess.fork(path.join(__dirname, 'example.js'), {
stdio: 'pipe',
env: {
...process.env,
GRACEFUL_SHUTDOWN_PORT: `${51095 + i}`,
GRACEFUL_SHUTDOWN_DELAY: '100',
GRACEFUL_SHUTDOWN_USE_EXIT_0: 'true',
GRACEFUL_SHUTDOWN_TIMEOUT: timeout,
},
})

const logLines = []
proc.on('exit', (code) => {
expect(code).to.eq(shouldTimeout ? 1 : 0)
const hasSeenShutdownTimeout = logLines.some((line) =>
line.includes('Terminate process after timeout'),
)
const hasSeenGracefulShutdown = logLines.some((line) =>
line.includes('graceful shutdown complete'),
)
expect(
hasSeenGracefulShutdown,
'seen graceful shutdown log message',
).to.eq(!shouldTimeout)
expect(
hasSeenShutdownTimeout,
'seen shutdown timeout log message',
).to.eq(shouldTimeout)
done()
})

// Send kill signal on first data chunk
proc.stdout.once('data', () =>
expect(proc.kill(killSignal), `${killSignal} success`).to.eq(true),
)

// See if we've seen the appropriate log message
proc.stdout.on('data', (chunk) => {
logLines.push(chunk.toString().trim())
})
})
})
})

it('work without logger enabled', async () => {
const fastify = Fastify({
logger: false,
Expand Down

0 comments on commit cac9c26

Please sign in to comment.