├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ ├── pkg.pr.new.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── express_with_cluster │ ├── README.md │ ├── app.cjs │ ├── dispatch.cjs │ └── worker.cjs └── failure.cjs ├── package.json ├── src └── index.ts ├── test ├── fixtures │ ├── app.cjs │ ├── foo.js │ ├── ignore.cjs │ └── worker.cjs ├── graceful.test.ts ├── ignore.test.ts └── worker.test.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest, macos-latest' 15 | version: '18.19.0, 18, 20, 22' 16 | secrets: 17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - run: corepack enable 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Build 21 | run: npm run prepublishOnly --if-present 22 | 23 | - run: npx pkg-pr-new publish 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | .tshy* 17 | .eslintcache 18 | dist 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.0](https://github.com/node-modules/graceful/compare/v1.1.0...v2.0.0) (2024-12-15) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * drop Node.js < 18.19.0 support 9 | 10 | part of https://github.com/eggjs/egg/issues/3644 11 | 12 | https://github.com/eggjs/egg/issues/5257 13 | 14 | closes https://github.com/node-modules/graceful/issues/16 15 | 16 | 18 | ## Summary by CodeRabbit 19 | 20 | - **New Features** 21 | - Introduced a new ESLint configuration for TypeScript and Node.js. 22 | - Added a new GitHub Actions workflow for package publishing. 23 | - Implemented a new TypeScript configuration for enhanced type safety. 24 | - Created a new test suite for validating worker process behavior. 25 | - Added a new test suite for verifying child process behavior. 26 | 27 | - **Bug Fixes** 28 | - Updated Node.js CI workflow to include newer versions and improved 29 | configurations. 30 | 31 | - **Documentation** 32 | - Enhanced README with updated badges and installation instructions. 33 | - Updated example documentation to reflect file extension changes. 34 | 35 | - **Chores** 36 | - Removed outdated files and workflows to streamline the repository. 37 | - Updated `.gitignore` to exclude additional files and directories. 38 | 39 | 40 | ### Features 41 | 42 | * support cjs and esm both by tshy ([#17](https://github.com/node-modules/graceful/issues/17)) ([7192a67](https://github.com/node-modules/graceful/commit/7192a67f5beeee90e085417287ad3918c21dd271)) 43 | 44 | 1.1.0 / 2022-09-22 45 | ================== 46 | 47 | **features** 48 | * [[`e571409`](http://github.com/node-modules/graceful/commit/e571409d957cf1f209b4d61e7e3e4ede4babc76f)] - feat: support ignoreCode (#13) (hyj1991 <>) 49 | 50 | **others** 51 | * [[`52f008a`](http://github.com/node-modules/graceful/commit/52f008a3ed71764e288cf35281c87ab1ead3176f)] - 🤖 TEST: Use Github Action (fengmk2 <>) 52 | 53 | 1.0.2 / 2018-10-31 54 | ================== 55 | 56 | **others** 57 | * [[`fa719ff`](http://github.com/node-modules/graceful/commit/fa719ff9c9793c28b624a919b92bfb2a269547c6)] - fix: graceful exit kill children (#12) (Yiyu He <>) 58 | 59 | 1.0.1 / 2016-06-23 60 | ================== 61 | 62 | * fix: print more server connections log (#9) 63 | * fix: ignore GRACEFUL_COV env 64 | 65 | 1.0.0 / 2014-11-05 66 | ================== 67 | 68 | * refator: use express instead connect on example 69 | 70 | 0.1.0 / 2014-05-29 71 | ================== 72 | 73 | * send disconnect message 74 | 75 | 0.0.6 / 2014-02-17 76 | ================== 77 | 78 | * add console.error(err.stack) by default (@dead-horse) 79 | * add npm image 80 | * support coveralls 81 | 82 | 0.0.5 / 2013-04-18 83 | ================== 84 | 85 | * fixed header sent bug 86 | 87 | 0.0.4 / 2013-04-18 88 | ================== 89 | 90 | * add options.worker 91 | * add custom error log demo 92 | 93 | 0.0.3 / 2013-04-14 94 | ================== 95 | 96 | * Let http server set `Connection: close` header, and close the current request socket. fixed #2 97 | 98 | 0.0.2 / 2013-04-14 99 | ================== 100 | 101 | * Support multi servers close fixed #1 102 | * update readme 103 | 104 | 0.0.1 / 2013-04-12 105 | ================== 106 | 107 | * first commit 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 - 2014 fengmk2 4 | Copyright (c) 2015 - present node-modules and other contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graceful 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Test coverage][cov-image]][cov-url] 5 | [![npm download][download-image]][download-url] 6 | [![Node.js Version](https://img.shields.io/node/v/graceful.svg?style=flat)](https://nodejs.org/en/download/) 7 | 8 | [npm-image]: https://img.shields.io/npm/v/graceful.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/graceful 10 | [cov-image]: https://codecov.io/github/node-modules/graceful/coverage.svg?branch=master 11 | [cov-url]: https://codecov.io/github/node-modules/graceful?branch=master 12 | [download-image]: https://img.shields.io/npm/dm/graceful.svg?style=flat-square 13 | [download-url]: https://npmjs.org/package/graceful 14 | 15 | Graceful exit when `uncaughtException` emit, base on `process.on('uncaughtException')`. 16 | 17 | ## Why we should use this module 18 | 19 | It's the best way to handle `uncaughtException` on current situations. 20 | 21 | * [Node.js 异步异常的处理与domain模块解析](http://deadhorse.me/nodejs/2013/04/13/exception_and_domain.html) 22 | 23 | ## Install 24 | 25 | ```bash 26 | npm install graceful 27 | ``` 28 | 29 | ## Usage 30 | 31 | Please see [express_with_cluster](https://github.com/node-modules/graceful/tree/master/example/express_with_cluster) example. 32 | 33 | This below code just for dev demo, don't use it on production env: 34 | 35 | ```js 36 | const express = require('express'); 37 | const { graceful } = require('graceful'); 38 | 39 | const app = express() 40 | .use() 41 | .use(function(req, res){ 42 | if (Math.random() > 0.5) { 43 | foo.bar(); 44 | } 45 | setTimeout(function() { 46 | if (Math.random() > 0.5) { 47 | throw new Error('Asynchronous error from timeout'); 48 | } else { 49 | res.end('Hello from Connect!'); 50 | } 51 | }, 100); 52 | setTimeout(function() { 53 | if (Math.random() > 0.5) { 54 | throw new Error('Mock second error'); 55 | } 56 | }, 200); 57 | }) 58 | .use(function(err, req, res, next) { 59 | res.end(err.message); 60 | }); 61 | 62 | const server = app.listen(1984); 63 | 64 | graceful({ 65 | servers: [server], 66 | killTimeout: '30s', 67 | }); 68 | ``` 69 | 70 | If you have multi servers on one process, you just add them to `server`: 71 | 72 | ```js 73 | graceful({ 74 | servers: [server1, server2, restapi], 75 | killTimeout: '15s', 76 | }); 77 | ``` 78 | 79 | ### ESM and TypeScript 80 | 81 | ```ts 82 | import { graceful } from 'graceful'; 83 | ``` 84 | 85 | ## Contributors 86 | 87 | [![Contributors](https://contrib.rocks/image?repo=node-modules/graceful)](https://github.com/node-modules/graceful/graphs/contributors) 88 | 89 | Made with [contributors-img](https://contrib.rocks). 90 | 91 | ## License 92 | 93 | [MIT](LICENSE) 94 | -------------------------------------------------------------------------------- /example/express_with_cluster/README.md: -------------------------------------------------------------------------------- 1 | # express with cluster example 2 | 3 | * Master: dispatch.cjs 4 | * Worker: worker.cjs 5 | * Your application logic: app.cjs 6 | 7 | ## Run 8 | 9 | ```bash 10 | $ node example/express_with_cluster/dispatch.cjs 11 | ``` 12 | 13 | ## Test 14 | 15 | curl asyncerror twice: 16 | 17 | ```bash 18 | $ curl localhost:1337/asyncerror 19 | 20 | $ curl localhost:1337/asyncerror 21 | 22 | ``` 23 | 24 | [dispatch.cjs](https://github.com/node-modules/graceful/blob/master/example/express_with_cluster/dispatch.cjs) stdout: 25 | 26 | ```bash 27 | $ node example/express_with_cluster/dispatch.cjs 28 | [Thu Apr 11 2013 18:45:36 GMT+0800 (CST)] [worker:21711] start listen on 1337 29 | [Thu Apr 11 2013 18:45:36 GMT+0800 (CST)] [worker:21712] start listen on 1337 30 | [uncaughtException] throw error 1 times 31 | [ReferenceError: foo is not defined] 32 | [Fri Apr 12 2013 18:11:34 GMT+0800 (CST)] [worker:52207] close server! 33 | [Fri Apr 12 2013 18:11:34 GMT+0800 (CST)] [worker:52207] worker disconnect! 34 | [Fri Apr 12 2013 18:11:34 GMT+0800 (CST)] [master:52205] wroker:52207 disconnect! new worker:52317 fork 35 | [Fri Apr 12 2013 18:11:34 GMT+0800 (CST)] [worker:52317] start listen on 1337 36 | [uncaughtException] throw error 1 times 37 | [ReferenceError: foo is not defined] 38 | [Fri Apr 12 2013 18:11:35 GMT+0800 (CST)] [worker:52206] close server! 39 | [Fri Apr 12 2013 18:11:35 GMT+0800 (CST)] [worker:52206] worker disconnect! 40 | [Fri Apr 12 2013 18:11:35 GMT+0800 (CST)] [master:52205] wroker:52206 disconnect! new worker:52328 fork 41 | [Fri Apr 12 2013 18:11:35 GMT+0800 (CST)] [worker:52328] start listen on 1337 42 | [Fri Apr 12 2013 18:11:37 GMT+0800 (CST)] [worker:52207] kill timeout, exit now. 43 | [Fri Apr 12 2013 18:11:37 GMT+0800 (CST)] [master:52205] wroker:52207 exit! 44 | ``` 45 | -------------------------------------------------------------------------------- /example/express_with_cluster/app.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var express = require('express'); 5 | 6 | var app = express(); 7 | app.use(function (req, res, next) { 8 | req.on('end', function () { 9 | if (req.url === '/asyncerror') { 10 | setTimeout(function () { 11 | foo.bar(); 12 | }, 10); 13 | return; 14 | } 15 | process.nextTick(function () { 16 | res.setHeader('content-type', 'text/json'); 17 | res.end(JSON.stringify({ 18 | method: req.method, 19 | url: req.url, 20 | headers: req.headers, 21 | Connection: res.getHeader('connection') || 'keep-alive', 22 | pid: process.pid, 23 | })); 24 | }); 25 | }); 26 | req.resume(); 27 | }) 28 | .use(function (err, req, res, next) { 29 | var domainThrown = err.domain_thrown || err.domainThrown; 30 | var msg = 'domainThrown: ' + domainThrown + '\n' + err.stack; 31 | console.error('%s %s\n%s', req.method, req.url, msg); 32 | res.statusCode = 500; 33 | res.setHeader('content-type', 'text/plain'); 34 | res.end(msg + '\n'); 35 | }); 36 | 37 | var server = http.createServer(app); 38 | module.exports = server; 39 | -------------------------------------------------------------------------------- /example/express_with_cluster/dispatch.cjs: -------------------------------------------------------------------------------- 1 | // http://nodejs.org/docs/latest/api/domain.html#domain_warning_don_t_ignore_errors 2 | var cluster = require('cluster'); 3 | var path = require('path'); 4 | 5 | cluster.setupMaster({ 6 | exec: path.join(__dirname, 'worker.cjs') 7 | }); 8 | 9 | // In real life, you'd probably use more than just 2 workers, 10 | // and perhaps not put the master and worker in the same file. 11 | // 12 | // You can also of course get a bit fancier about logging, and 13 | // implement whatever custom logic you need to prevent DoS 14 | // attacks and other bad behavior. 15 | // 16 | // See the options in the cluster documentation. 17 | // 18 | // The important thing is that the master does very little, 19 | // increasing our resilience to unexpected errors. 20 | 21 | cluster.fork(); 22 | cluster.fork(); 23 | 24 | // when worker disconnect, fork a new one 25 | cluster.on('disconnect', function (worker) { 26 | var w = cluster.fork(); 27 | console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', 28 | Date(), process.pid, worker.process.pid, w.process.pid); 29 | }); 30 | 31 | // if you do not want every disconnect fork a new worker. 32 | // you can listen worker's message. 33 | // graceful will send `graceful:disconnect` message when disconnect. 34 | 35 | // cluster.on('fork', function(worker) { 36 | // worker.on('message', function (msg) { 37 | // if (msg === 'graceful:disconnect') { 38 | // var w = cluster.fork(); 39 | // console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', 40 | // new Date(), process.pid, worker.process.pid, w.process.pid); 41 | // } 42 | // }); 43 | // }); 44 | 45 | cluster.on('exit', function (worker) { 46 | console.error('[%s] [master:%s] worker:%s exit!', 47 | Date(), process.pid, worker.process.pid); 48 | }); 49 | -------------------------------------------------------------------------------- /example/express_with_cluster/worker.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var PORT = +process.env.PORT || 1337; 4 | var { graceful } = require('../../'); 5 | var server = require('./app'); 6 | server.listen(PORT); 7 | console.log('[%s] [worker:%s] web server start listen on %s', new Date(), process.pid, PORT); 8 | 9 | var restapi = require('http').createServer().listen(1985); 10 | console.log('[%s] [worker:%s] rest api start listen on %s', new Date(), process.pid, 1985); 11 | 12 | graceful({ 13 | server: [server, restapi], 14 | killTimeout: 10000, 15 | error: function (err, throwErrorCount) { 16 | // you can do custom log here, send email, call phone and so on... 17 | if (err.message) { 18 | err.message += ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')'; 19 | } 20 | // logger.error(err); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /example/failure.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var http = require('http'); 4 | var express = require('express'); 5 | var { graceful } = require('../'); 6 | 7 | var keepAliveClient = http.request({ 8 | host: 'www.google.com', 9 | path: '/index.html' 10 | }); 11 | 12 | var app = express() 13 | .use(function (req, res) { 14 | if (!keepAliveClient) { 15 | setTimeout(function () { 16 | foo2.bar(); 17 | }, 10); 18 | return; 19 | } 20 | keepAliveClient.on('response', function (response) { 21 | foo.bar(); 22 | }); 23 | keepAliveClient.end(); 24 | keepAliveClient = null; 25 | }) 26 | .use(function (err, req, res, next) { 27 | res.end(err.message); 28 | }); 29 | 30 | app = app.listen(1984); 31 | 32 | var app1 = express().listen(1985); 33 | 34 | graceful({ 35 | server: [app, app1], 36 | killTimeout: '10s', 37 | }); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graceful", 3 | "version": "2.0.0", 4 | "description": "Graceful exit when `uncaughtException` emit, base on `process.on('uncaughtException')`.", 5 | "homepage": "https://github.com/node-modules/graceful", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/node-modules/graceful.git" 9 | }, 10 | "keywords": [ 11 | "graceful", 12 | "uncaught", 13 | "uncaughtException", 14 | "error", 15 | "graceful", 16 | "cluster", 17 | "graceful exit" 18 | ], 19 | "author": "fengmk2 ", 20 | "license": "MIT", 21 | "engines": { 22 | "node": ">= 18.19.0" 23 | }, 24 | "dependencies": { 25 | "@fengmk2/ps-tree": "^2.0.2", 26 | "humanize-ms": "^2.0.0" 27 | }, 28 | "devDependencies": { 29 | "@arethetypeswrong/cli": "^0.17.1", 30 | "@eggjs/tsconfig": "1", 31 | "@types/express": "^5.0.0", 32 | "@types/mocha": "10", 33 | "@types/node": "22", 34 | "@types/supertest": "^6.0.2", 35 | "egg-bin": "6", 36 | "eslint": "8", 37 | "eslint-config-egg": "14", 38 | "express": "^4.21.2", 39 | "mm": "^3.4.0", 40 | "supertest": "^7.0.0", 41 | "tshy": "3", 42 | "tshy-after": "1", 43 | "typescript": "5" 44 | }, 45 | "scripts": { 46 | "lint": "eslint --cache src test --ext .ts", 47 | "pretest": "npm run lint -- --fix && npm run prepublishOnly", 48 | "test": "egg-bin test", 49 | "preci": "npm run lint && npm run prepublishOnly && attw --pack", 50 | "ci": "egg-bin cov", 51 | "prepublishOnly": "tshy && tshy-after" 52 | }, 53 | "type": "module", 54 | "tshy": { 55 | "exports": { 56 | ".": "./src/index.ts", 57 | "./package.json": "./package.json" 58 | } 59 | }, 60 | "exports": { 61 | ".": { 62 | "import": { 63 | "types": "./dist/esm/index.d.ts", 64 | "default": "./dist/esm/index.js" 65 | }, 66 | "require": { 67 | "types": "./dist/commonjs/index.d.ts", 68 | "default": "./dist/commonjs/index.js" 69 | } 70 | }, 71 | "./package.json": "./package.json" 72 | }, 73 | "files": [ 74 | "dist", 75 | "src" 76 | ], 77 | "types": "./dist/commonjs/index.d.ts", 78 | "main": "./dist/commonjs/index.js", 79 | "module": "./dist/esm/index.js" 80 | } 81 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'node:http'; 2 | import cluster, { type Worker } from 'node:cluster'; 3 | import { debuglog } from 'node:util'; 4 | import { ms } from 'humanize-ms'; 5 | import { pstree } from '@fengmk2/ps-tree'; 6 | 7 | const debug = debuglog('graceful'); 8 | 9 | export interface GracefulOptions { 10 | /** 11 | * servers, we need to close it and stop taking new requests. 12 | */ 13 | servers?: Server[] | Server; 14 | /** 15 | * @deprecated please use `servers` instead 16 | */ 17 | server?: Server[] | Server; 18 | /** 19 | * worker suicide timeout, default is 30 seconds 20 | */ 21 | killTimeout?: number | string; 22 | /** 23 | * when uncaughtException emit, error(err, count). 24 | * You can log error here. 25 | */ 26 | error?: (err: Error, throwErrorCount: number) => void; 27 | /** 28 | * worker contains `disconnect()` 29 | */ 30 | worker?: Worker; 31 | /** 32 | * ignore error code 33 | */ 34 | ignoreCode?: number[]; 35 | } 36 | 37 | /** 38 | * graceful, please use with `cluster` in production env. 39 | */ 40 | export function graceful(options: GracefulOptions) { 41 | const killTimeout = ms(options.killTimeout ?? '30s'); 42 | const onError = options.error || function() {}; 43 | let servers = options.servers ?? options.server ?? []; 44 | const ignoreCode = options.ignoreCode ?? []; 45 | if (!Array.isArray(servers)) { 46 | servers = [ servers ]; 47 | } 48 | if (servers.length === 0) { 49 | throw new TypeError('options.servers required!'); 50 | } 51 | 52 | let throwErrorCount = 0; 53 | process.on('uncaughtException', err => { 54 | throwErrorCount += 1; 55 | onError(err, throwErrorCount); 56 | console.error('[%s] [graceful:worker:%s:uncaughtException] throw error %d times', 57 | Date(), process.pid, throwErrorCount); 58 | console.error(err); 59 | console.error(err.stack); 60 | const errorCode = Reflect.get(err, 'code'); 61 | if (ignoreCode.includes(errorCode)) { 62 | console.error('Error code: %s matches ignore list: %j, don\'t exit.', errorCode, ignoreCode); 63 | return; 64 | } 65 | 66 | if (throwErrorCount > 1) { 67 | return; 68 | } 69 | 70 | servers.forEach(server => { 71 | if (server instanceof Server) { 72 | server.on('request', (req, res) => { 73 | // Let http server set `Connection: close` header, and close the current request socket. 74 | // req.shouldKeepAlive = false; 75 | Reflect.set(req, 'shouldKeepAlive', false); 76 | res.shouldKeepAlive = false; 77 | if (!res.headersSent) { 78 | res.setHeader('Connection', 'close'); 79 | } 80 | }); 81 | } 82 | }); 83 | 84 | // make sure we close down within `killTimeout` seconds 85 | const killTimer = setTimeout(async () => { 86 | console.error('[%s] [graceful:worker:%s] kill timeout, exit now. NODE_ENV: %s', 87 | Date(), process.pid, process.env.NODE_ENV); 88 | if (process.env.NODE_ENV !== 'test') { 89 | // kill children by SIGKILL before exit 90 | await killChildren(); 91 | process.exit(1); 92 | } 93 | }, killTimeout); 94 | console.error('[%s] [graceful:worker:%s] will exit after %dms', 95 | Date(), process.pid, killTimeout); 96 | 97 | // But don't keep the process open just for that! 98 | // If there is no more io waiting, just let process exit normally. 99 | killTimer.unref(); 100 | 101 | const worker = options.worker || cluster.worker; 102 | 103 | // cluster mode 104 | if (worker) { 105 | try { 106 | // stop taking new requests. 107 | // because server could already closed, need try catch the error: `Error: Not running` 108 | for (const [ i, server ] of servers.entries()) { 109 | server.close(); 110 | console.error('[%s] [graceful:worker:%s] close server#%s, connections: %s', 111 | Date(), process.pid, i, server.connections); 112 | } 113 | console.error('[%s] [graceful:worker:%s] close %d servers!', 114 | Date(), process.pid, servers.length); 115 | } catch (err: any) { 116 | // Usually, this error throw cause by the active connections after the first domain error, 117 | // oh well, not much we can do at this point. 118 | console.error('[%s] [graceful:worker:%s] Error on server close!\n%s', 119 | Date(), process.pid, err.stack); 120 | } 121 | 122 | try { 123 | // Let the master know we're dead. This will trigger a 124 | // 'disconnect' in the cluster master, and then it will fork 125 | // a new worker. 126 | worker.send('graceful:disconnect'); 127 | worker.disconnect(); 128 | console.error('[%s] [graceful:worker:%s] worker disconnect!', 129 | Date(), process.pid); 130 | } catch (err: any) { 131 | // Usually, this error throw cause by the active connections after the first domain error, 132 | // oh well, not much we can do at this point. 133 | console.error('[%s] [graceful:worker:%s] Error on worker disconnect!\n%s', 134 | Date(), process.pid, err.stack); 135 | } 136 | } 137 | }); 138 | } 139 | 140 | async function killChildren() { 141 | try { 142 | const children = await pstree(process.pid); 143 | for (const child of children) { 144 | killProcess(parseInt(child.PID)); 145 | } 146 | console.error('[%s] [graceful:worker:%s] pstree find %d children and killed', 147 | Date(), process.pid, children.length); 148 | } catch (err) { 149 | // if get children error, just ignore it 150 | console.error('[%s] [graceful:worker:%s] pstree find children error: %s', 151 | Date(), process.pid, err); 152 | } 153 | } 154 | 155 | function killProcess(pid: number) { 156 | try { 157 | process.kill(pid, 'SIGKILL'); 158 | } catch (err) { 159 | // ignore 160 | debug('kill %s error: %s', pid, err); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /test/fixtures/app.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { fork } = require('node:child_process'); 3 | const path = require('node:path'); 4 | const { createServer } = require('node:http'); 5 | const { graceful } = require('../../'); 6 | 7 | fork(path.join(__dirname, 'worker.cjs')); 8 | 9 | const server = createServer(); 10 | server.listen(); 11 | graceful({ 12 | server, 13 | killTimeout: 2000, 14 | }); 15 | 16 | setTimeout(function() { 17 | throw new Error('wow'); 18 | }, 100); 19 | -------------------------------------------------------------------------------- /test/fixtures/foo.js: -------------------------------------------------------------------------------- 1 | console.log('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/ignore.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { fork } = require('node:child_process'); 3 | const path = require('node:path'); 4 | const { createServer } = require('node:http'); 5 | const { graceful } = require('../../'); 6 | 7 | fork(path.join(__dirname, 'worker.cjs')); 8 | 9 | const server = createServer(); 10 | server.listen(); 11 | graceful({ 12 | server, 13 | killTimeout: 2000, 14 | ignoreCode: [ 'EMOCKERROR' ], 15 | }); 16 | 17 | setTimeout(function() { 18 | const error = new Error('mock'); 19 | error.code = 'EMOCKERROR'; 20 | throw error; 21 | }, 1000); 22 | -------------------------------------------------------------------------------- /test/fixtures/worker.cjs: -------------------------------------------------------------------------------- 1 | console.log('worker1 [%s] started', process.pid); 2 | 3 | setTimeout(function() { 4 | console.log('worker1 alived'); 5 | }, 1000); 6 | 7 | setInterval(function() { 8 | // keep alive 9 | }, 100000); 10 | 11 | process.on('SIGTERM', function() { 12 | console.log('worker1 on sigterm and not exit'); 13 | }); 14 | -------------------------------------------------------------------------------- /test/graceful.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import http from 'node:http'; 3 | import path from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import request from 'supertest'; 6 | import express from 'express'; 7 | import { graceful } from '../src/index.js'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | describe('test/graceful.test.ts', () => { 13 | function normalHandler(req: any, res: any) { 14 | if (req.url === '/sync_error') { 15 | throw new Error('sync_error'); 16 | } 17 | if (req.url === '/async_error') { 18 | process.nextTick(function() { 19 | // @ts-ignore 20 | ff.foo(); 21 | }); 22 | return; 23 | } 24 | if (req.url === '/async_error_twice') { 25 | setTimeout(function() { 26 | // @ts-ignore 27 | ff.foo(); 28 | }, 100); 29 | setTimeout(function() { 30 | // @ts-ignore 31 | bar.bar(); 32 | }, 200); 33 | return; 34 | } 35 | if (req.url === '/async_error_triple') { 36 | setTimeout(function() { 37 | // @ts-ignore 38 | ff.foo(); 39 | }, 100); 40 | setTimeout(function() { 41 | // @ts-ignore 42 | bar.bar(); 43 | }, 200); 44 | setTimeout(function() { 45 | // @ts-ignore 46 | hehe.bar(); 47 | }, 200); 48 | return; 49 | } 50 | res.end(req.url); 51 | } 52 | 53 | function errorHandler(err: any, _req: any, res: any) { 54 | res.statusCode = 500; 55 | res.end(err.message); 56 | } 57 | 58 | const server = http.createServer(); 59 | graceful({ server, killTimeout: '1s' }); 60 | 61 | const app = express() 62 | .use('/public', express.static(__dirname + '/fixtures')) 63 | .use(normalHandler) 64 | .use(errorHandler); 65 | 66 | server.on('request', app); 67 | 68 | it('should GET / status 200', function(done) { 69 | request(server) 70 | .get('/') 71 | .expect(200, done); 72 | }); 73 | 74 | it('should GET /public/foo.js status 200', function(done) { 75 | request(server) 76 | .get('/public/foo.js') 77 | .expect('console.log(\'bar\');\n') 78 | .expect(200, done); 79 | }); 80 | 81 | it('should GET /sync_error status 500', function(done) { 82 | request(server) 83 | .get('/sync_error') 84 | .expect(/sync_error/) 85 | .expect(500, done); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/ignore.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { fork } from 'node:child_process'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | describe('test/ignore.test.ts', () => { 10 | it('should kill all children', function(done) { 11 | const app = fork(path.join(__dirname, 'fixtures/ignore.cjs')); 12 | setTimeout(function() { 13 | assert(alive(app.pid!)); 14 | }, 1000); 15 | 16 | setTimeout(function() { 17 | assert(alive(app.pid!)); 18 | done(); 19 | }, 4000); 20 | }); 21 | }); 22 | 23 | function alive(pid: number) { 24 | try { 25 | process.kill(pid, 0); 26 | return true; 27 | } catch (err) { 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/worker.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { fork } from 'node:child_process'; 5 | import { pstree } from '@fengmk2/ps-tree'; 6 | import mm from 'mm'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | describe('test/worker.test.ts', () => { 12 | afterEach(mm.restore); 13 | 14 | it('should kill all children', done => { 15 | mm(process.env, 'NODE_ENV', 'prod'); 16 | const app = fork(path.join(__dirname, 'fixtures/app.cjs')); 17 | let workerPid: string; 18 | setTimeout(() => { 19 | assert(alive(app.pid!), 'app.pid should alive'); 20 | pstree(app.pid!, (err, children) => { 21 | if (err) { 22 | return done(err); 23 | } 24 | assert(children); 25 | assert.equal(children.length, 1); 26 | workerPid = children[0].PID; 27 | }); 28 | }, 1000); 29 | 30 | setTimeout(() => { 31 | assert(!alive(app.pid!), 'app.pid should not alive'); 32 | assert(!alive(Number(workerPid)), 'workerPid should not alive'); 33 | done(); 34 | }, 4000); 35 | }); 36 | }); 37 | 38 | function alive(pid: number) { 39 | try { 40 | process.kill(pid, 0); 41 | console.warn('%s alive', pid); 42 | return true; 43 | } catch (err) { 44 | console.error('kill %s error: %s', pid, err); 45 | return false; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | --------------------------------------------------------------------------------