Skip to content
This repository has been archived by the owner on Jul 17, 2023. It is now read-only.

Commit

Permalink
feat(cli): integrate @strv/heimdall 🚀
Browse files Browse the repository at this point in the history
  • Loading branch information
robertrossmann committed Apr 26, 2019
1 parent d48d589 commit c2ffe52
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 138 deletions.
5 changes: 5 additions & 0 deletions packages/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/package.json
Expand Up @@ -10,6 +10,7 @@
"contributors": [],
"dependencies": {
"@atlas.js/repl": "^2.1.2",
"@strv/heimdall": "^1.0.0",
"caporal": "^1.1.0",
"source-map-support": "^0.5.0"
},
Expand Down
73 changes: 10 additions & 63 deletions packages/cli/src/commands/start.mjs
@@ -1,4 +1,5 @@
import * as cluster from 'cluster'
import { heimdall } from '@strv/heimdall'
import Command from '../command'

class Start extends Command {
Expand All @@ -9,78 +10,24 @@ class Start extends Command {
// ['--cluster <size>', 'Start the app clustered with <size> worker processes', caporal.INT],
]

doExit = () => this.exit()

async run() {
process.once('SIGINT', this.doExit)
process.once('SIGTERM', this.doExit)
process.once('beforeExit', this.doExit)
await this.atlas.start()
.catch(fatal)
}

/**
* Cleanly shut down the Atlas instance so that the process may exit
*
* @private
* @return {Promise}
*/
exit() {
// Prevent calling this.atlas.stop() multiple times when repeatedly pressing ctrl-c. Next time
// you press ctrl+c, we'll just brute-force-quit the whole thing. 🔥
process.removeListener('SIGINT', this.doExit)
process.removeListener('SIGTERM', this.doExit)
process.removeListener('beforeExit', this.doExit)
process.once('SIGINT', forcequit)
process.once('SIGTERM', forcequit)
const atlas = this.atlas

return this.atlas.stop()
.then(() => {
process.removeListener('SIGINT', forcequit)
process.removeListener('SIGTERM', forcequit)
await heimdall({
async execute() {
await atlas.start()
},
async exit() {
await atlas.stop()

// If this is a clusterised worker, disconnect the worker from the master once we are done
// here so the process can exit gracefully without the master having to do anything special.
if (cluster.isWorker) {
cluster.worker.disconnect()
}
})
.catch(fatal)
},
})
}
}


/**
* Cause the process to forcefully shut down by throwing an uncaught exception
*
* @private
* @return {void}
*/
function forcequit() {
process.removeListener('SIGINT', forcequit)
process.removeListener('SIGTERM', forcequit)

throw new Error('Forced quit')
}

/**
* Handle a fatal error in the start/stop methods of the Atlas instance
*
* @private
* @param {Error} err The error object which caused the fatal error
* @return {void}
*/
function fatal(err) {
process.exitCode = 1
// eslint-disable-next-line no-console
console.error(err.stack)

// A fatal error occured. We have no guarantee that the instance will shut down properly. We will
// wait 10 seconds to see if it manages to shut down gracefully, then we will use brute force to
// stop the process. 💣
// eslint-disable-next-line no-process-exit
setTimeout(() => process.exit(), 10000)
.unref()
}

export default Start
86 changes: 11 additions & 75 deletions packages/cli/test/commands/start.test.mjs
Expand Up @@ -4,16 +4,15 @@ import Start from '../../src/commands/start'
describe('CLI: start', () => {
let start

beforeEach(() => {
beforeEach(function() {
start = new Start()
start.atlas = {
start: sinon.stub().resolves(),
stop: sinon.stub().resolves(),
}
})

afterEach(() => start.doExit())

this.sandbox.stub(process, 'once')
})

it('exists', () => {
expect(Start).to.be.a('function')
Expand All @@ -37,93 +36,30 @@ describe('CLI: start', () => {
expect(start.atlas.start).to.have.callCount(1)
})

it('registers SIGINT, SIGTERM and beforeExit event listeners', async () => {
const counts = {
SIGINT: process.listenerCount('SIGINT'),
SIGTERM: process.listenerCount('SIGTERM'),
beforeExit: process.listenerCount('beforeExit'),
}

await start.run()

expect(process.listenerCount('SIGINT')).to.equal(counts.SIGINT + 1)
expect(process.listenerCount('SIGTERM')).to.equal(counts.SIGTERM + 1)
expect(process.listenerCount('beforeExit')).to.equal(counts.beforeExit + 1)
})

it('removes SIGINT, SIGTERM and beforeExit listeners upon receiving a signal', async () => {
const counts = {
SIGINT: process.listenerCount('SIGINT'),
SIGTERM: process.listenerCount('SIGTERM'),
beforeExit: process.listenerCount('beforeExit'),
}

await start.run()
await start.doExit()

expect(process.listenerCount('SIGINT')).to.equal(counts.SIGINT)
expect(process.listenerCount('SIGTERM')).to.equal(counts.SIGTERM)
expect(process.listenerCount('beforeExit')).to.equal(counts.beforeExit)
})

it('stops the atlas instance upon exit', async () => {
// Sanity check
expect(start.atlas.stop).to.have.callCount(0)

it('stops the atlas instance on signal', async () => {
await start.run()
await start.doExit()
// Grab Heimdall's listener and invoke it to fake a signal
const onsignal = process.once.lastCall.args[1]
await onsignal()

expect(start.atlas.stop).to.have.callCount(1)
})


it('disconnects from cluster master upon exit', async () => {
const disconnect = sinon.stub()
cluster.isWorker = true
cluster.worker = { disconnect }

await start.run()
await start.doExit()
// Grab Heimdall's listener and invoke it to fake a signal
const onsignal = process.once.lastCall.args[1]
await onsignal()

cluster.isWorker = false
delete cluster.worker

expect(disconnect).to.have.callCount(1)
})

it('throws an error to stop the process if a terminating signal is sent again', async () => {
await start.run()
start.doExit()

// Sending SIGINT causes the test suite to exit with code 130, so just test SIGTERM 🤷‍♂️
expect(() => process.emit('SIGTERM')).to.throw(Error, /Forced quit/u)

delete process.exitCode
})

it('kills the process after 10s if an error occurs during stop procedure', async function() {
this.sandbox.stub(process, 'exit')
this.sandbox.stub(console, 'error')

const error = new Error('u-oh')
const clock = sinon.useFakeTimers({
toFake: ['setTimeout'],
})

start.atlas.stop.rejects(error)
await start.doExit()

// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(error.stack)
expect(process.exit).to.have.callCount(0)

clock.runAll()
clock.restore()

expect(process.exit).to.have.callCount(1)
expect(process.exitCode).to.equal(1)
expect(clock.now).to.equal(10000)

delete process.exitCode
})
})
})

0 comments on commit c2ffe52

Please sign in to comment.