-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): allow application to trap shutdown signals
This is useful for kubernetes deployment as SIGTERM is sent to the process to request graceful shutdown.
- Loading branch information
1 parent
7c4cb01
commit 0db5415
Showing
3 changed files
with
189 additions
and
0 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
packages/core/src/__tests__/acceptance/application-with-shutdown.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Copyright IBM Corp. 2019. All Rights Reserved. | ||
// Node module: @loopback/core | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {promisify} from 'util'; | ||
import {Application, LifeCycleObserver} from '../..'; | ||
const sleep = promisify(setTimeout); | ||
|
||
function main() { | ||
// Optional argument as the grace period | ||
const gracePeriod = Number.parseFloat(process.argv[2]); | ||
|
||
class MyTimer implements LifeCycleObserver { | ||
timer: NodeJS.Timer; | ||
|
||
start() { | ||
console.log('start'); | ||
this.timer = setTimeout(() => { | ||
console.log('timeout'); | ||
}, 30000); | ||
} | ||
|
||
async stop() { | ||
console.log('stop'); | ||
clearTimeout(this.timer); | ||
if (gracePeriod >= 0) { | ||
// Set a longer sleep to trigger force kill | ||
await sleep(gracePeriod + 100); | ||
} | ||
console.log('stopped'); | ||
} | ||
} | ||
|
||
const app = new Application({ | ||
shutdown: {signals: ['SIGTERM', 'SIGINT'], gracePeriod}, | ||
}); | ||
app.lifeCycleObserver(MyTimer); | ||
app.start().catch(err => { | ||
console.error(err); | ||
process.exit(1); | ||
}); | ||
} | ||
|
||
if (require.main === module) { | ||
main(); | ||
} |
97 changes: 97 additions & 0 deletions
97
packages/core/src/__tests__/acceptance/application.shutdown.acceptance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// Copyright IBM Corp. 2019. All Rights Reserved. | ||
// Node module: @loopback/core | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {expect, skipIf} from '@loopback/testlab'; | ||
import {ChildProcess, fork} from 'child_process'; | ||
|
||
const app = require.resolve('./application-with-shutdown'); | ||
const isWindows = process.platform === 'win32'; | ||
|
||
describe('Application shutdown hooks', () => { | ||
skipIf(isWindows, it, 'traps registered signals - SIGTERM', () => { | ||
return testSignal('SIGTERM'); | ||
}); | ||
|
||
skipIf( | ||
isWindows, | ||
it, | ||
'traps registered signals with grace period - SIGTERM', | ||
() => { | ||
// No 'stopped` is recorded | ||
return testSignal('SIGTERM', 5, ['start\n', 'stop\n']); | ||
}, | ||
); | ||
|
||
skipIf(isWindows, it, 'traps registered signals - SIGINT', () => { | ||
return testSignal('SIGINT'); | ||
}); | ||
|
||
skipIf(isWindows, it, 'does not trap unregistered signals - SIGHUP', () => { | ||
return testSignal('SIGHUP', undefined, ['start\n']); | ||
}); | ||
|
||
function normalizeStdoutData(output: string[]) { | ||
// The received events can be `['start\n', 'stop\nstopped\n']` instead | ||
// of [ 'start\n', 'stop\n', 'stopped\n' ] | ||
return output.join(''); | ||
} | ||
|
||
function createAppWithShutdown( | ||
expectedSignal: NodeJS.Signals, | ||
gracePeriod: number | undefined, | ||
expectedEvents: string[], | ||
) { | ||
let args: string[] = []; | ||
if (typeof gracePeriod === 'number') { | ||
args = [gracePeriod.toString()]; | ||
} | ||
const child = fork(app, args, { | ||
stdio: 'pipe', | ||
}); | ||
const events: string[] = []; | ||
// Wait until the child process logs `start` | ||
const childStart = new Promise<ChildProcess>(resolve => { | ||
child.stdout!.on('data', (buf: Buffer) => { | ||
events.push(buf.toString('utf-8')); | ||
resolve(child); | ||
}); | ||
}); | ||
// Wait until the child process exits | ||
const childExit = new Promise((resolve, reject) => { | ||
child.on('exit', (code, sig) => { | ||
if (typeof sig === 'string') { | ||
// FIXME(rfeng): For some reason, the sig can be null | ||
expect(sig).to.eql(expectedSignal); | ||
} | ||
// The received events can be `['start\n', 'stop\nstopped\n']` instead | ||
// of [ 'start\n', 'stop\n', 'stopped\n' ] | ||
expect(normalizeStdoutData(events)).to.eql( | ||
normalizeStdoutData(expectedEvents), | ||
); | ||
resolve(); | ||
}); | ||
child.on('error', err => { | ||
reject(err); | ||
}); | ||
}); | ||
return {childStart, childExit}; | ||
} | ||
|
||
async function testSignal( | ||
expectedSignal: NodeJS.Signals, | ||
gracePeriod: number | undefined = undefined, | ||
expectedEvents = ['start\n', 'stop\n', 'stopped\n'], | ||
) { | ||
const {childStart, childExit} = createAppWithShutdown( | ||
expectedSignal, | ||
gracePeriod, | ||
expectedEvents, | ||
); | ||
const child = await childStart; | ||
// Send SIGTERM signal to the child process | ||
child.kill(expectedSignal); | ||
return childExit; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters