@ladjs/graceful
Advanced tools
Comparing version 0.0.1 to 1.0.0
186
index.js
@@ -1,3 +0,1 @@ | ||
const { promisify } = require('util'); | ||
const stopAgenda = require('stop-agenda'); | ||
const debug = require('debug')('@ladjs/graceful'); | ||
@@ -7,14 +5,11 @@ | ||
constructor(config) { | ||
this.config = Object.assign( | ||
{ | ||
server: false, | ||
redisClient: false, | ||
mongoose: false, | ||
agenda: false, | ||
logger: console, | ||
stopAgenda: {}, | ||
timeoutMs: 5000 | ||
}, | ||
config | ||
); | ||
this.config = { | ||
servers: [], | ||
redisClients: [], | ||
mongooses: [], | ||
bulls: [], | ||
logger: console, | ||
timeoutMs: 5000, | ||
...config | ||
}; | ||
@@ -26,2 +21,14 @@ // shortcut logger | ||
this._isExiting = false; | ||
// bind this to everything | ||
this.listen = this.listen.bind(this); | ||
this.stopServer = this.stopServer.bind(this); | ||
this.stopServers = this.stopServers.bind(this); | ||
this.stopRedisClient = this.stopRedisClient.bind(this); | ||
this.stopRedisClients = this.stopRedisClients.bind(this); | ||
this.stopMongoose = this.stopMongoose.bind(this); | ||
this.stopMongooses = this.stopMongooses.bind(this); | ||
this.stopBull = this.stopBull.bind(this); | ||
this.stopBulls = this.stopBulls.bind(this); | ||
this.exit = this.exit.bind(this); | ||
} | ||
@@ -37,3 +44,3 @@ | ||
// handle uncaught exceptions | ||
process.on('uncaughtException', err => { | ||
process.once('uncaughtException', err => { | ||
this.logger.error(err); | ||
@@ -45,64 +52,93 @@ process.exit(1); | ||
// <http://pm2.keymetrics.io/docs/usage/signals-clean-restart/#windows-graceful-stop> | ||
process.on('message', msg => { | ||
if (msg === 'shutdown') this.exit(); | ||
process.on('message', async message => { | ||
if (message === 'shutdown') { | ||
this.logger.info('Received shutdown message'); | ||
await this.exit(); | ||
} | ||
}); | ||
// handle graceful restarts | ||
process.on('SIGTERM', () => this.exit()); | ||
process.on('SIGHUP', () => this.exit()); | ||
process.on('SIGINT', () => this.exit()); | ||
// support nodemon (SIGUSR2 as well) | ||
// <https://github.com/remy/nodemon#controlling-shutdown-of-your-script> | ||
['SIGTERM', 'SIGHUP', 'SIGINT', 'SIGUSR2'].forEach(sig => { | ||
process.once(sig, async () => { | ||
await this.exit(sig); | ||
}); | ||
}); | ||
} | ||
exit() { | ||
const { server, redisClient, mongoose, agenda, timeoutMs } = this.config; | ||
async stopServer(server) { | ||
try { | ||
await server.close(); | ||
} catch (err) { | ||
this.config.logger.error(err); | ||
} | ||
} | ||
if (this._isExiting) { | ||
debug('already in the process of a graceful exit'); | ||
return; | ||
async stopServers() { | ||
await Promise.all( | ||
this.config.servers.map(server => this.stopServer(server)) | ||
); | ||
} | ||
async stopRedisClient(client) { | ||
if (client.status === 'end') return; | ||
// TODO: give it a max of 500ms | ||
// https://github.com/OptimalBits/bull/blob/develop/lib/queue.js#L516 | ||
try { | ||
await client.quit(); | ||
} catch (err) { | ||
this.config.logger.error(err); | ||
} | ||
} | ||
this._isExiting = true; | ||
async stopRedisClients() { | ||
await Promise.all( | ||
this.config.redisClients.map(client => this.stopRedisClient(client)) | ||
); | ||
} | ||
debug('graceful exit started'); | ||
async stopMongoose(mongoose) { | ||
try { | ||
await mongoose.disconnect(); | ||
} catch (err) { | ||
this.config.logger.error(err); | ||
} | ||
} | ||
const promises = []; | ||
async stopMongooses() { | ||
await Promise.all( | ||
this.config.mongooses.map(mongoose => this.stopMongoose(mongoose)) | ||
); | ||
} | ||
if (server) promises.push(promisify(server.close).bind(server)); | ||
async stopBull(bull) { | ||
try { | ||
await bull.close(); | ||
} catch (err) { | ||
this.config.logger.error(err); | ||
} | ||
} | ||
if (redisClient) | ||
promises.push(promisify(redisClient.quit).bind(redisClient)); | ||
async stopBulls() { | ||
await Promise.all(this.config.bulls.map(bull => this.stopBull(bull))); | ||
} | ||
if (mongoose) { | ||
// we need to stop agenda and cancel recurring jobs | ||
// before shutting down our mongodb connection | ||
// otherwise cancellation wouldn't work gracefully | ||
if (agenda) { | ||
promises.push( | ||
new Promise(async (resolve, reject) => { | ||
try { | ||
try { | ||
await stopAgenda(agenda, this.config.stopAgenda); | ||
} catch (err) { | ||
this.logger.error(err); | ||
} finally { | ||
await mongoose.disconnect(); | ||
resolve(); | ||
} | ||
} catch (err) { | ||
reject(err); | ||
} | ||
}) | ||
); | ||
} else { | ||
promises.push(mongoose.disconnect); | ||
} | ||
} else if (agenda) { | ||
promises.push(stopAgenda(agenda, this.config.stopAgenda)); | ||
async exit(code) { | ||
if (code) this.logger.info(`Gracefully exiting from ${code}`); | ||
if (this._isExiting) { | ||
this.logger.info('Graceful exit already in progress'); | ||
return; | ||
} | ||
// give it only X ms to gracefully shut down | ||
this._isExiting = true; | ||
debug('graceful exit started'); | ||
// give it only X ms to gracefully exit | ||
setTimeout(() => { | ||
this.logger.error( | ||
new Error( | ||
`graceful shutdown failed, timeout of ${timeoutMs} ms exceeded` | ||
`Graceful exit failed, timeout of ${this.config.timeoutMs}ms was exceeded` | ||
) | ||
@@ -112,15 +148,23 @@ ); | ||
process.exit(1); | ||
}, timeoutMs); | ||
}, this.config.timeoutMs); | ||
Promise.all(promises) | ||
.then(() => { | ||
this.logger.debug('gracefully shut down'); | ||
// eslint-disable-next-line unicorn/no-process-exit | ||
process.exit(0); | ||
}) | ||
.catch(err => { | ||
this.logger.error(err); | ||
// eslint-disable-next-line unicorn/no-process-exit | ||
process.exit(1); | ||
}); | ||
try { | ||
await Promise.all([ | ||
// servers | ||
this.stopServers(), | ||
// redisClients | ||
this.stopRedisClients(), | ||
// mongooses | ||
this.stopMongooses(), | ||
// bulls | ||
this.stopBulls() | ||
]); | ||
this.logger.info('Gracefully exited'); | ||
// eslint-disable-next-line unicorn/no-process-exit | ||
process.exit(0); | ||
} catch (err) { | ||
this.logger.error(err); | ||
// eslint-disable-next-line unicorn/no-process-exit | ||
process.exit(1); | ||
} | ||
} | ||
@@ -127,0 +171,0 @@ } |
105
package.json
{ | ||
"name": "@ladjs/graceful", | ||
"description": | ||
"Gracefully exit server (Koa), database (Mongo/Mongoose), and job scheduler (Agenda)", | ||
"version": "0.0.1", | ||
"description": "Gracefully exit server (Koa), database (Mongo/Mongoose), and job scheduler (Redis/Bull)", | ||
"version": "1.0.0", | ||
"author": "Nick Baugh <niftylettuce@gmail.com> (http://niftylettuce.com/)", | ||
@@ -11,2 +10,7 @@ "bugs": { | ||
}, | ||
"commitlint": { | ||
"extends": [ | ||
"@commitlint/config-conventional" | ||
] | ||
}, | ||
"contributors": [ | ||
@@ -16,19 +20,19 @@ "Nick Baugh <niftylettuce@gmail.com> (http://niftylettuce.com/)" | ||
"dependencies": { | ||
"debug": "^3.0.1", | ||
"stop-agenda": "^0.0.5" | ||
"debug": "^4.1.1" | ||
}, | ||
"devDependencies": { | ||
"ava": "^0.22.0", | ||
"codecov": "^2.3.0", | ||
"cross-env": "^5.0.5", | ||
"eslint": "^4.5.0", | ||
"eslint-config-prettier": "^2.3.0", | ||
"eslint-plugin-prettier": "^2.2.0", | ||
"husky": "^0.14.3", | ||
"lint-staged": "^4.0.4", | ||
"nyc": "^11.1.0", | ||
"prettier": "^1.6.1", | ||
"remark-cli": "^4.0.0", | ||
"remark-preset-github": "^0.0.6", | ||
"xo": "^0.19.0" | ||
"@commitlint/cli": "^8.1.0", | ||
"@commitlint/config-conventional": "^8.1.0", | ||
"ava": "^2.3.0", | ||
"codecov": "^3.5.0", | ||
"cross-env": "^5.2.1", | ||
"eslint": "6.3.0", | ||
"eslint-config-xo-lass": "^1.0.3", | ||
"fixpack": "^2.3.1", | ||
"husky": "^3.0.5", | ||
"lint-staged": "^9.2.5", | ||
"nyc": "^14.1.1", | ||
"remark-cli": "^7.0.0", | ||
"remark-preset-github": "^0.0.16", | ||
"xo": "^0.24.0" | ||
}, | ||
@@ -39,14 +43,37 @@ "engines": { | ||
"homepage": "https://github.com/ladjs/graceful", | ||
"keywords": ["@ladjs/graceful", "lass"], | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged && npm test", | ||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" | ||
} | ||
}, | ||
"keywords": [ | ||
"@ladjs/graceful", | ||
"lass" | ||
], | ||
"license": "MIT", | ||
"lint-staged": { | ||
"*.{js,jsx,mjs,ts,tsx,css,less,scss,json,graphql}": [ | ||
"prettier --write --single-quote --trailing-comma none", | ||
"*.js": [ | ||
"xo --fix", | ||
"git add" | ||
], | ||
"*.md": ["remark . -qfo", "git add"] | ||
"*.md": [ | ||
"remark . -qfo", | ||
"git add" | ||
], | ||
"package.json": [ | ||
"fixpack", | ||
"git add" | ||
] | ||
}, | ||
"main": "index.js", | ||
"prettier": { | ||
"singleQuote": true, | ||
"bracketSpacing": true, | ||
"trailingComma": "none" | ||
}, | ||
"remarkConfig": { | ||
"plugins": ["preset-github"] | ||
"plugins": [ | ||
"preset-github" | ||
] | ||
}, | ||
@@ -60,3 +87,2 @@ "repository": { | ||
"lint": "xo && remark . -qfo", | ||
"precommit": "lint-staged && npm test", | ||
"test": "npm run lint && npm run test-coverage", | ||
@@ -66,29 +92,8 @@ "test-coverage": "cross-env NODE_ENV=test nyc ava" | ||
"xo": { | ||
"extends": "prettier", | ||
"plugins": ["prettier"], | ||
"parserOptions": { | ||
"sourceType": "script" | ||
}, | ||
"rules": { | ||
"prettier/prettier": [ | ||
"error", | ||
{ | ||
"singleQuote": true, | ||
"bracketSpacing": true, | ||
"trailingComma": "none" | ||
} | ||
], | ||
"max-len": [ | ||
"error", | ||
{ | ||
"code": 80, | ||
"ignoreUrls": true | ||
} | ||
], | ||
"capitalized-comments": "off", | ||
"camelcase": "off", | ||
"no-warning-comments": "off" | ||
}, | ||
"space": true | ||
"prettier": true, | ||
"space": true, | ||
"extends": [ | ||
"xo-lass" | ||
] | ||
} | ||
} |
@@ -10,3 +10,3 @@ # [**@ladjs/graceful**](https://github.com/ladjs/graceful) | ||
> Gracefully exit server (Koa), database (Mongo/Mongoose), and job scheduler (Agenda) | ||
> Gracefully exit server (Koa), database (Mongo/Mongoose), Redis clients, and job scheduler (Redis/Bull) | ||
@@ -43,64 +43,19 @@ | ||
* `process.on('unhandledRejection')` - will output via `config.logger.error` | ||
* `process.on('uncaughtException')` - will output via `config.logger.error` and `process.exit(1)` | ||
* `process.once('uncaughtException')` - will output via `config.logger.error` and `process.exit(1)` (_does not exit gracefully_) | ||
* `process.on('message')` - support Windows (e.g. signals not available) and listen for message of `shutdown` and then exit gracefully | ||
* `process.on('SIGTERM')` - will exit gracefully | ||
* `process.on('SIGHUP')` - will exit gracefully | ||
* `process.on('SIGINT')` - will exit gracefully | ||
* `process.once('SIGTERM')` - will exit gracefully | ||
* `process.once('SIGHUP')` - will exit gracefully | ||
* `process.once('SIGINT')` - will exit gracefully | ||
* `process.once('SIGUSR2')` - will exit gracefully (nodemon support) | ||
This package also prevents multiple process/SIG events from triggering multiple graceful exits. Only one graceful exit can occur at a time. | ||
An example below shows usage showing `server`, `redisClient`, `mongoose`, and `agenda` options all being passed. | ||
See one of these following files from [Lad][] for the most up to date usage example: | ||
Please note that NONE of these are required, as each one is completely optional and independent. | ||
* API - <https://github.com/ladjs/lad/blob/master/template/api.js> | ||
* Web - <https://github.com/ladjs/lad/blob/master/template/web.js> | ||
* Bull - <https://github.com/ladjs/lad/blob/master/template/bull.js> | ||
* Proxy - <https://github.com/ladjs/lad/blob/master/template/proxy.js> | ||
However if you have both `mongoose` and `agenda` defined, it knows to stop `agenda` first using [stop-agenda][], then disconnect `mongoose`. | ||
```js | ||
const http = require('http'); | ||
const Graceful = require('@ladjs/graceful'); | ||
const Mongoose = require('@ladjs/mongoose'); | ||
const Koa = require('koa'); | ||
const redis = require('redis'); | ||
const Agenda = require('agenda'); | ||
const app = new Koa(); | ||
const agenda = new Agenda(); | ||
let server = http.createServer(app.callback()); | ||
server = server.listen(); | ||
const redisClient = redis.createClient(); | ||
const mongoose = new Mongoose({ agenda }).mongoose; | ||
const graceful = new Graceful({ | ||
// uses `server.close` for graceful exit | ||
server, | ||
// uses `redisClient.quit` for graceful exit | ||
redisClient, | ||
// uses `mongoose.disconnect` for graceful exit | ||
mongoose, | ||
// uses `stop-agenda` package for graceful exit | ||
agenda, | ||
// default logger (you can also use `new Logger()` from @ladjs/logger) | ||
logger: console, | ||
// options get passed to `stop-agenda` | ||
// <https://github.com/ladjs/stop-agenda> | ||
stopAgenda: {}, | ||
// max time allowed in ms for graceful exit | ||
timeoutMs: 5000 | ||
}); | ||
graceful.listen(); | ||
``` | ||
## Contributors | ||
@@ -124,2 +79,2 @@ | ||
[stop-agenda]: https://github.com/ladjs/stop-agenda | ||
[lad]: https://lad.js.org |
const test = require('ava'); | ||
const Graceful = require('../'); | ||
const Graceful = require('..'); | ||
@@ -5,0 +5,0 @@ test('returns itself', t => { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1
149
0
10516
14
9
78
1
+ Addeddebug@4.3.7(transitive)
- Removedstop-agenda@^0.0.5
- Removeddebug@3.2.7(transitive)
- Removedstop-agenda@0.0.5(transitive)
Updateddebug@^4.1.1