Skip to content

Commit 54e5504

Browse files
authored
feat(shutdown): Adds option to exit(o) instead of resending the uncatched signal that was received (#212)
1 parent d02b0de commit 54e5504

File tree

6 files changed

+11468
-58
lines changed

6 files changed

+11468
-58
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const options = {
6464
timeout: 1000, // [optional = 1000] number of milliseconds before forceful exiting
6565
signal, // [optional = 'SIGTERM'] what signal to listen for relative to shutdown
6666
signals, // [optional = []] array of signals to listen for relative to shutdown
67+
useExit0, // [optional = false] instead of sending the received signal again without beeing catched, the process will exit(0)
6768
sendFailuresDuringShutdown, // [optional = true] whether or not to send failure (503) during shutdown
6869
beforeShutdown, // [optional] called before the HTTP server starts its shutdown
6970
onSignal, // [optional] cleanup function, returning a promise (used to be onSigterm)
@@ -183,13 +184,18 @@ server.listen(PORT || 3000);
183184

184185
When Kubernetes or a user deletes a Pod, Kubernetes will notify it and wait for `gracePeriod` seconds before killing it.
185186

186-
During that time window (30 seconds by default), the Pod is in the `terminating` state and will be removed from any Services by a controller. The Pod itself needs to catch the `SIGTERM` signal and start failing any readiness probes.
187+
During that time window (30 seconds by default), the Pod is in the `terminating` state and will be removed from any Services by a controller.
188+
The Pod itself needs to catch the `SIGTERM` signal and start failing any readiness probes.
187189

188190
> If the ingress controller you use route via the Service, it is not an issue for your case. At the time of this writing, we use the nginx ingress controller which routes traffic directly to the Pods.
189191
190192
During this time, it is possible that load-balancers (like the nginx ingress controller) don't remove the Pods "in time", and when the Pod dies, it kills live connections.
191193

192-
To make sure you don't lose any connections, we recommend delaying the shutdown with the number of milliseconds that's defined by the readiness probe in your deployment configuration. To help with this, terminus exposes an option called `beforeShutdown` that takes any Promise-returning function.
194+
To make sure you don't lose any connections, we recommend delaying the shutdown with the number of milliseconds that's defined by the readiness probe in your deployment configuration.
195+
To help with this, terminus exposes an option called `beforeShutdown` that takes any Promise-returning function.
196+
197+
Also it makes sense to use the `useExit0 = true` option to signal Kubernetes that the container exited gracefully.
198+
Otherwise APM's will send you alerts, in some cases.
193199

194200
```javascript
195201
function beforeShutdown () {
@@ -201,7 +207,8 @@ function beforeShutdown () {
201207
})
202208
}
203209
createTerminus(server, {
204-
beforeShutdown
210+
beforeShutdown,
211+
useExit0: true
205212
})
206213
```
207214

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict'
2+
const http = require('http')
3+
const server = http.createServer((req, res) => res.end('hello'))
4+
5+
const { createTerminus } = require('../../')
6+
7+
createTerminus(server, {
8+
useExit0: (process.argv[2] === 'true')
9+
})
10+
11+
server.listen(8000, () => {
12+
process.kill(process.pid, 'SIGTERM')
13+
})

lib/terminus.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function decorateWithHealthCheck (server, state, options) {
126126
}
127127

128128
function decorateWithSignalHandler (server, state, options) {
129-
const { signals, onSignal, beforeShutdown, onShutdown, timeout, logger } = options
129+
const { signals, useExit0, onSignal, beforeShutdown, onShutdown, timeout, logger } = options
130130

131131
stoppable(server, timeout)
132132

@@ -140,8 +140,14 @@ function decorateWithSignalHandler (server, state, options) {
140140
await asyncServerStop()
141141
await onSignal()
142142
await onShutdown()
143-
signals.forEach(sig => process.removeListener(sig, cleanup))
144-
process.kill(process.pid, signal)
143+
if (useExit0) {
144+
// Exit process
145+
process.exit(0)
146+
} else {
147+
// Resend recieved signal but remove traps beforehand
148+
signals.forEach(sig => process.removeListener(sig, cleanup))
149+
process.kill(process.pid, signal)
150+
}
145151
} catch (error) {
146152
logger('error happened during shutdown', error)
147153
process.exit(1)
@@ -168,6 +174,7 @@ function terminus (server, options = {}) {
168174
const {
169175
signal = 'SIGTERM',
170176
signals = [],
177+
useExit0 = false,
171178
timeout = 1000,
172179
healthChecks = {},
173180
sendFailuresDuringShutdown = true,
@@ -201,6 +208,7 @@ function terminus (server, options = {}) {
201208
if (!signals.includes(signal)) signals.push(signal)
202209
decorateWithSignalHandler(server, state, {
203210
signals,
211+
useExit0,
204212
onSignal,
205213
beforeShutdown,
206214
onShutdown,

lib/terminus.spec.js

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -658,25 +658,38 @@ describe('Terminus', () => {
658658
})
659659

660660
it('accepts custom headers', async () => {
661-
662-
createTerminus(server, {
663-
healthChecks: {
664-
'/health': () => {
665-
return Promise.resolve()
661+
createTerminus(server, {
662+
healthChecks: {
663+
'/health': () => {
664+
return Promise.resolve()
665+
}
666+
},
667+
headers: {
668+
'Access-Control-Allow-Origin': '*',
669+
'Access-Control-Allow-Methods': 'OPTIONS, POST, GET'
666670
}
667-
},
668-
headers: {
669-
"Access-Control-Allow-Origin": "*",
670-
"Access-Control-Allow-Methods": "OPTIONS, POST, GET",
671-
},
671+
})
672+
server.listen(8000)
673+
674+
const res = await fetch('http://localhost:8000/health')
675+
expect(res.status).to.eql(200)
676+
expect(res.headers.has('Access-Control-Allow-Methods')).to.eql(true)
677+
expect(res.headers.get('Access-Control-Allow-Methods')).to.eql('OPTIONS, POST, GET')
678+
expect(res.headers.has('Access-Control-Allow-Origin')).to.eql(true)
679+
expect(res.headers.get('Access-Control-Allow-Origin')).to.eql('*')
672680
})
673-
server.listen(8000)
674-
675-
const res = await fetch('http://localhost:8000/health')
676-
expect(res.status).to.eql(200)
677-
expect(res.headers.has('Access-Control-Allow-Methods')).to.eql(true)
678-
expect(res.headers.get('Access-Control-Allow-Methods')).to.eql('OPTIONS, POST, GET')
679-
expect(res.headers.has('Access-Control-Allow-Origin')).to.eql(true)
680-
expect(res.headers.get('Access-Control-Allow-Origin')).to.eql('*')
681+
682+
describe('useExit0', () => {
683+
it('allows to exit(0) with useExit0 - true', async () => {
684+
const result = spawnSync('node', ['lib/standalone-tests/terminus.useExit0.js', 'true'])
685+
expect(result.status).to.eql(0)
686+
expect(result.signal).to.eql(null)
687+
})
688+
689+
it('allows to exit(0) with useExit0 - false', async () => {
690+
const result = spawnSync('node', ['lib/standalone-tests/terminus.useExit0.js', 'false'])
691+
expect(result.status).to.eql(null)
692+
expect(result.signal).to.eql('SIGTERM')
693+
})
681694
})
682-
})
695+
})

0 commit comments

Comments
 (0)