Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add timeout to stop and killProcess #228

Merged
merged 3 commits into from
Jun 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,17 @@ Start the daemon.
`callback` is a function with the signature `function(err, ipfsApi)` that receives an instance of `ipfs-api` on success or an instance of `Error` on failure


#### `ipfsd.stop([callback])`
#### `ipfsd.stop([timeout, callback])`

Stop the daemon.

`callback` is a function with the signature `function(err)` callback - function that receives an instance of `Error` on failure
`callback` is a function with the signature `function(err)` callback - function that receives an instance of `Error` on failure. Use timeout to specify the grace period in ms before hard stopping the daemon. Otherwise, a grace period of `10500` ms will be used for disposable nodes and `10500 * 3` ms for non disposable nodes.

#### `ipfsd.killProcess([callback])`
#### `ipfsd.killProcess([timeout, callback])`

Kill the `ipfs daemon` process.
Kill the `ipfs daemon` process. Use timeout to specify the grace period in ms before hard stopping the daemon. Otherwise, a grace period of `10500` ms will be used for disposable nodes and `10500 * 3` ms for non disposable nodes.

Note: timeout is ignored for `proc` nodes

First a `SIGTERM` is sent, after 10.5 seconds `SIGKILL` is sent if the process hasn't exited yet.

Expand Down
6 changes: 4 additions & 2 deletions src/endpoint/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ module.exports = (server) => {
path: '/stop',
handler: (request, reply) => {
const id = request.query.id
nodes[id].stop((err) => {
const timeout = request.payload.timeout
nodes[id].stop(timeout, (err) => {
if (err) {
return reply(boom.badRequest(err))
}
Expand All @@ -213,7 +214,8 @@ module.exports = (server) => {
path: '/kill',
handler: (request, reply) => {
const id = request.query.id
nodes[id].killProcess((err) => {
const timeout = request.payload.timeout
nodes[id].killProcess(timeout, (err) => {
if (err) {
return reply(boom.badRequest(err))
}
Expand Down
20 changes: 17 additions & 3 deletions src/ipfsd-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,21 @@ class DaemonClient {
/**
* Stop the daemon.
*
* @param {integer|undefined} timeout - Grace period to wait before force stopping the node
* @param {function(Error)} [cb]
* @returns {undefined}
*/
stop (cb) {
stop (timeout, cb) {
if (typeof timeout === 'function') {
cb = timeout
timeout = undefined
}

cb = cb || (() => {})
request
.post(`${this.baseUrl}/stop`)
.query({ id: this._id })
.send({ timeout })
.end((err) => {
if (err) {
return cb(new Error(err.response.body.message))
Expand All @@ -169,17 +176,24 @@ class DaemonClient {
/**
* Kill the `ipfs daemon` process.
*
* First `SIGTERM` is sent, after 7.5 seconds `SIGKILL` is sent
* First `SIGTERM` is sent, after 10.5 seconds `SIGKILL` is sent
* if the process hasn't exited yet.
*
* @param {integer|undefined} timeout - Grace period to wait before force stopping the node
* @param {function()} [cb] - Called when the process was killed.
* @returns {undefined}
*/
killProcess (cb) {
killProcess (timeout, cb) {
if (typeof timeout === 'function') {
cb = timeout
timeout = undefined
}

cb = cb || (() => {})
request
.post(`${this.baseUrl}/kill`)
.query({ id: this._id })
.send({ timeout })
.end((err) => {
if (err) {
return cb(new Error(err.response.body.message))
Expand Down
36 changes: 29 additions & 7 deletions src/ipfsd-daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const findIpfsExecutable = require('./utils/find-ipfs-executable')
const setConfigValue = require('./utils/set-config-value')
const run = require('./utils/run')

const GRACE_PERIOD = 10500 // amount of ms to wait before sigkill
// amount of ms to wait before sigkill
const GRACE_PERIOD = 10500

// amount of ms to wait before sigkill for non disposable repos
const NON_DISPOSABLE_GRACE_PERIOD = 10500 * 3

/**
* ipfsd for a go-ipfs or js-ipfs daemon
Expand Down Expand Up @@ -284,17 +288,23 @@ class Daemon {
/**
* Stop the daemon.
*
* @param {integer|undefined} timeout - Grace period to wait before force stopping the node
* @param {function(Error)} callback
* @returns {undefined}
*/
stop (callback) {
stop (timeout, callback) {
if (typeof timeout === 'function') {
callback = timeout
timeout = null
}

callback = callback || function noop () {}

if (!this.subprocess) {
return callback()
}

this.killProcess(callback)
this.killProcess(timeout, callback)
}

/**
Expand All @@ -304,20 +314,32 @@ class Daemon {
* process.kill(`SIGTERM`) is used. In either case, if the process
* does not exit after 10.5 seconds then a `SIGKILL` is used.
*
* @param {integer|undefined} timeout - Grace period to wait before force stopping the node
* @param {function()} callback - Called when the process was killed.
* @returns {undefined}
*/
killProcess (callback) {
killProcess (timeout, callback) {
if (typeof timeout === 'function') {
callback = timeout
timeout = null
}

if (!timeout) {
timeout = this.disposable
? GRACE_PERIOD
: NON_DISPOSABLE_GRACE_PERIOD
}

// need a local var for the closure, as we clear the var.
const subprocess = this.subprocess
const timeout = setTimeout(() => {
const grace = setTimeout(() => {
log('kill timeout, using SIGKILL', subprocess.pid)
subprocess.kill('SIGKILL')
}, GRACE_PERIOD)
}, timeout)

subprocess.once('exit', () => {
log('killed', subprocess.pid)
clearTimeout(timeout)
clearTimeout(grace)
this.subprocess = null
this._started = false
if (this.disposable) {
Expand Down
42 changes: 42 additions & 0 deletions test/endpoint/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,48 @@ describe('client', () => {
})
})

describe('.stop with timeout', () => {
describe('handle valid', () => {
after(() => {
mock.clearRoutes()
})

it('should handle valid request', (done) => {
mock.post('http://localhost:9999/stop', (req) => {
expect(req.query.id).to.exist()
})

node.stop(1000, (err) => {
expect(err).to.not.exist()
done()
})
})
})

describe('handle invalid', () => {
after(() => {
mock.clearRoutes()
})

it('should handle invalid request', (done) => {
mock.post('http://localhost:9999/stop', () => {
const badReq = boom.badRequest()
return {
status: badReq.output.statusCode,
body: {
message: badReq.message
}
}
})

node.stop((err) => {
expect(err).to.exist()
done()
})
})
})
})

describe('.killProcess', () => {
describe('handle valid', () => {
after(() => {
Expand Down
30 changes: 27 additions & 3 deletions test/endpoint/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ const routes = proxyquire(
cb(null, api)
}

node.stop = (cb) => node.killProcess(cb)
node.stop = (timeout, cb) => node.killProcess(timeout, cb)

node.killProcess = (cb) => {
node.killProcess = (timeout, cb) => {
node.started = false
cb()
}
Expand Down Expand Up @@ -213,7 +213,7 @@ describe('routes', () => {
})

describe('POST /stop', () => {
it('should return 200', (done) => {
it('should return 200 without timeout', (done) => {
server.inject({
method: 'POST',
url: `/stop?id=${id}`,
Expand All @@ -225,6 +225,18 @@ describe('routes', () => {
})
})

it('should return 200 with timeout', (done) => {
server.inject({
method: 'POST',
url: `/stop?id=${id}`,
headers: { 'content-type': 'application/json' },
payload: { id, timeout: 1000 }
}, (res) => {
expect(res.statusCode).to.equal(200)
done()
})
})

it('should return 400', (done) => {
server.inject({
method: 'POST',
Expand All @@ -250,6 +262,18 @@ describe('routes', () => {
})
})

it('should return 200 with timeout', (done) => {
server.inject({
method: 'POST',
url: `/kill?id=${id}`,
headers: { 'content-type': 'application/json' },
payload: { id, timeout: 1000 }
}, (res) => {
expect(res.statusCode).to.equal(200)
done()
})
})

it('should return 400', (done) => {
server.inject({
method: 'POST',
Expand Down
112 changes: 112 additions & 0 deletions test/start-stop.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,118 @@ tests.forEach((fOpts) => {
})
})

describe('start and stop with timeout', () => {
let ipfsd
let repoPath
let api
let pid
let stopped = false

before(function (done) {
this.timeout(50 * 1000)

const f = IPFSFactory.create(dfConfig)

f.spawn({
init: true,
start: false,
disposable: true,
initOptions: { bits: fOpts.bits }
}, (err, _ipfsd) => {
expect(err).to.not.exist()
expect(_ipfsd).to.exist()

ipfsd = _ipfsd
repoPath = ipfsd.path
done()
})
})

it('should return a node', () => {
expect(ipfsd).to.exist()
})

it('daemon exec path should match type', () => {
expect(exec).to.include.string(ipfsd.exec)
})

it('daemon should not be running', (done) => {
ipfsd.pid((pid) => {
expect(pid).to.not.exist()
done()
})
})

it('.start', function (done) {
this.timeout(20 * 1000)

ipfsd.start((err, ipfs) => {
expect(err).to.not.exist()

ipfsd.pid((_pid) => {
pid = _pid
api = ipfs

expect(isrunning(pid)).to.be.ok()
done()
})
})
})

it('is running', () => {
expect(api.id).to.exist()
})

it('.stop with timeout', function (done) {
this.timeout(15000 + 10) // should not take longer than timeout
ipfsd.stop(15000, (err) => {
expect(err).to.not.exist()
stopped = !isrunning(pid)
expect(stopped).to.be.ok()
done()
})
})

it('is stopped', function (done) {
// shutdown grace period is already 10500
this.timeout(20 * 1000)

ipfsd.pid((pid) => {
expect(pid).to.not.exist()
expect(stopped).to.equal(true)
expect(fs.existsSync(path.join(ipfsd.path, 'repo.lock'))).to.not.be.ok()
expect(fs.existsSync(path.join(ipfsd.path, 'api'))).to.not.be.ok()
done()
})
})

it('repo should cleaned up', () => {
expect(fs.existsSync(repoPath)).to.not.be.ok()
})

it('fail on start with non supported flags', function (done) {
// TODO js-ipfs doesn't fail on unrecognized args.
// Decided what should be the desired behaviour
if (fOpts.type === 'js') { return this.skip() }

const df = IPFSFactory.create(dfConfig)

df.spawn({
start: false,
initOptions: { bits: fOpts.bits }
}, (err, ipfsd) => {
expect(err).to.not.exist()
ipfsd.start(['--should-not-exist'], (err) => {
expect(err).to.exist()
expect(err.message)
.to.match(/unknown option "should-not-exist"\n/)

done()
})
})
})
})

describe('start and stop with custom exec path', () => {
let ipfsd
before(function (done) {
Expand Down