diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..9bcdb46
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "extends": [
+ "eslint-config-egg/typescript",
+ "eslint-config-egg/lib/rules/enforce-node-prefix"
+ ]
+}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 2850efc..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- branches: [ "master" ]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [ "master" ]
- schedule:
- - cron: '44 21 * * 5'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'javascript' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
- with:
- category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index aebe8bb..63e9df1 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -3,16 +3,15 @@ name: CI
on:
push:
branches: [ master ]
-
pull_request:
branches: [ master ]
- workflow_dispatch: {}
-
jobs:
Job:
name: Node.js
- uses: artusjs/github-actions/.github/workflows/node-test.yml@v1
+ uses: node-modules/github-actions/.github/workflows/node-test.yml@master
with:
os: 'ubuntu-latest, macos-latest'
- version: '14, 16, 18'
+ version: '18.19.0, 18, 20, 22'
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
new file mode 100644
index 0000000..bac3fac
--- /dev/null
+++ b/.github/workflows/pkg.pr.new.yml
@@ -0,0 +1,23 @@
+name: Publish Any Commit
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - run: corepack enable
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build
+ run: npm run prepublishOnly --if-present
+
+ - run: npx pkg-pr-new publish
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1612587..1c6cbb1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,14 +4,10 @@ on:
push:
branches: [ master ]
- workflow_dispatch: {}
-
jobs:
release:
name: Node.js
- uses: artusjs/github-actions/.github/workflows/node-release.yml@v1
+ uses: node-modules/github-actions/.github/workflows/node-release.yml@master
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
- with:
- checkTest: false
diff --git a/.gitignore b/.gitignore
index a84002d..50717b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ results
node_modules
npm-debug.log
+.tshy*
+.eslintcache
+dist
diff --git a/History.md b/CHANGELOG.md
similarity index 100%
rename from History.md
rename to CHANGELOG.md
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a137bcd
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 - 2014 fengmk2
+Copyright (c) 2015 - present node-modules and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index 3ec5257..b8bf7aa 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,12 @@
[![NPM version][npm-image]][npm-url]
[![Test coverage][cov-image]][cov-url]
[![npm download][download-image]][download-url]
+[![Node.js Version](https://img.shields.io/node/v/graceful.svg?style=flat)](https://nodejs.org/en/download/)
[npm-image]: https://img.shields.io/npm/v/graceful.svg?style=flat-square
[npm-url]: https://npmjs.org/package/graceful
-[cov-image]: https://codecov.io/github/node-modules/cfork/coverage.svg?branch=master
-[cov-url]: https://codecov.io/github/node-modules/cfork?branch=master
+[cov-image]: https://codecov.io/github/node-modules/graceful/coverage.svg?branch=master
+[cov-url]: https://codecov.io/github/node-modules/graceful?branch=master
[download-image]: https://img.shields.io/npm/dm/graceful.svg?style=flat-square
[download-url]: https://npmjs.org/package/graceful
@@ -17,26 +18,25 @@ Graceful exit when `uncaughtException` emit, base on `process.on('uncaughtExcept
It's the best way to handle `uncaughtException` on current situations.
-* [domain failure](https://github.com/fengmk2/domain-middleware/blob/master/example/failure.js).
* [Node.js 异步异常的处理与domain模块解析](http://deadhorse.me/nodejs/2013/04/13/exception_and_domain.html)
## Install
```bash
-$ npm install graceful
+npm install graceful
```
## Usage
-Please see [connect_with_cluster](https://github.com/fengmk2/graceful/tree/master/example/connect_with_cluster) example.
+Please see [express_with_cluster](https://github.com/node-modules/graceful/tree/master/example/express_with_cluster) example.
This below code just for dev demo, don't use it on production env:
```js
-var express = require('express');
-var graceful = require('graceful');
+const express = require('express');
+const { graceful } = require('graceful');
-var app = express()
+const app = express()
.use()
.use(function(req, res){
if (Math.random() > 0.5) {
@@ -59,7 +59,7 @@ var app = express()
res.end(err.message);
});
-var server = app.listen(1984);
+const server = app.listen(1984);
graceful({
servers: [server],
@@ -76,43 +76,18 @@ graceful({
});
```
-If you are using [pm](https://github.com/aleafs/pm),
-you can follow the [graceful_exit with pm demo](https://github.com/aleafs/pm/tree/master/demo/graceful_exit).
+### ESM and TypeScript
-
+```ts
+import { graceful } from 'graceful';
+```
## Contributors
-|[
fengmk2](https://github.com/fengmk2)
|[
dead-horse](https://github.com/dead-horse)
|[
hyj1991](https://github.com/hyj1991)
|[
imyelo](https://github.com/imyelo)
|
-| :---: | :---: | :---: | :---: |
-
-
-This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Thu Sep 22 2022 19:41:57 GMT+0800`.
+[![Contributors](https://contrib.rocks/image?repo=node-modules/graceful)](https://github.com/node-modules/graceful/graphs/contributors)
-
+Made with [contributors-img](https://contrib.rocks).
## License
-(The MIT License)
-
-Copyright (c) 2013 - 2014 fengmk2 <fengmk2@gmail.com>
-Copyright (c) 2015 - 2016 node-modules and other contributors
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+[MIT](LICENSE)
diff --git a/example/express_with_cluster/README.md b/example/express_with_cluster/README.md
index 112ce45..d399e7c 100644
--- a/example/express_with_cluster/README.md
+++ b/example/express_with_cluster/README.md
@@ -1,13 +1,13 @@
# express with cluster example
-* Master: dispatch.js
-* Worker: worker.js
-* Your application logic: app.js
+* Master: dispatch.cjs
+* Worker: worker.cjs
+* Your application logic: app.cjs
## Run
```bash
-$ node example/express_with_cluster/dispatch.js
+$ node example/express_with_cluster/dispatch.cjs
```
## Test
@@ -21,10 +21,10 @@ $ curl localhost:1337/asyncerror
```
-[dispatch.js](https://github.com/node-modules/graceful/blob/master/example/express_with_cluster/dispatch.js) stdout:
+[dispatch.cjs](https://github.com/node-modules/graceful/blob/master/example/express_with_cluster/dispatch.cjs) stdout:
```bash
-$ node example/express_with_cluster/dispatch.js
+$ node example/express_with_cluster/dispatch.cjs
[Thu Apr 11 2013 18:45:36 GMT+0800 (CST)] [worker:21711] start listen on 1337
[Thu Apr 11 2013 18:45:36 GMT+0800 (CST)] [worker:21712] start listen on 1337
[uncaughtException] throw error 1 times
diff --git a/example/express_with_cluster/app.js b/example/express_with_cluster/app.cjs
similarity index 100%
rename from example/express_with_cluster/app.js
rename to example/express_with_cluster/app.cjs
diff --git a/example/express_with_cluster/dispatch.js b/example/express_with_cluster/dispatch.cjs
similarity index 96%
rename from example/express_with_cluster/dispatch.js
rename to example/express_with_cluster/dispatch.cjs
index 37b70a0..9824bb3 100644
--- a/example/express_with_cluster/dispatch.js
+++ b/example/express_with_cluster/dispatch.cjs
@@ -1,11 +1,9 @@
-"use strict";
-
// http://nodejs.org/docs/latest/api/domain.html#domain_warning_don_t_ignore_errors
var cluster = require('cluster');
var path = require('path');
cluster.setupMaster({
- exec: path.join(__dirname, 'worker.js')
+ exec: path.join(__dirname, 'worker.cjs')
});
// In real life, you'd probably use more than just 2 workers,
diff --git a/example/express_with_cluster/worker.js b/example/express_with_cluster/worker.cjs
similarity index 94%
rename from example/express_with_cluster/worker.js
rename to example/express_with_cluster/worker.cjs
index 2a51478..64ada2d 100644
--- a/example/express_with_cluster/worker.js
+++ b/example/express_with_cluster/worker.cjs
@@ -1,7 +1,7 @@
"use strict";
var PORT = +process.env.PORT || 1337;
-var graceful = require('../../');
+var { graceful } = require('../../');
var server = require('./app');
server.listen(PORT);
console.log('[%s] [worker:%s] web server start listen on %s', new Date(), process.pid, PORT);
diff --git a/example/failure.js b/example/failure.cjs
similarity index 94%
rename from example/failure.js
rename to example/failure.cjs
index 5fa4196..4ece362 100644
--- a/example/failure.js
+++ b/example/failure.cjs
@@ -2,7 +2,7 @@
var http = require('http');
var express = require('express');
-var graceful = require('../');
+var { graceful } = require('../');
var keepAliveClient = http.request({
host: 'www.google.com',
diff --git a/index.js b/index.js
deleted file mode 100644
index e9208ea..0000000
--- a/index.js
+++ /dev/null
@@ -1,142 +0,0 @@
-'use strict';
-
-var http = require('http');
-var cluster = require('cluster');
-var ms = require('humanize-ms');
-var pstree = require('ps-tree');
-
-/**
- * graceful, please use with `cluster` in production env.
- *
- * @param {Object} options
- * - {Array} servers, we need to close it and stop taking new requests.
- * - {Function(err, throwErrorCount)} [error], when uncaughtException emit, error(err, count).
- * You can log error here.
- * - {Number} [killTimeout], worker suicide timeout, default is 30 seconds.
- * - {Object} [worker], worker contains `disconnect()`.
- */
-module.exports = function graceful(options) {
- options = options || {};
- var killTimeout = ms(options.killTimeout || '30s');
- var onError = options.error || function () {};
- var servers = options.servers || options.server || [];
- var ignoreCode = options.ignoreCode || [];
- if (!Array.isArray(servers)) {
- servers = [servers];
- }
- if (servers.length === 0) {
- throw new TypeError('options.servers required!');
- }
-
- var throwErrorCount = 0;
- process.on('uncaughtException', function (err) {
- throwErrorCount += 1;
- onError(err, throwErrorCount);
- console.error('[%s] [graceful:worker:%s:uncaughtException] throw error %d times',
- Date(), process.pid, throwErrorCount);
- console.error(err);
- console.error(err.stack);
-
- if(ignoreCode.includes(err.code)) {
- console.error('Error code: %s matches ignore list: %s, don\'t exit.', err.code, '[ ' + ignoreCode.join(', ') + ' ]');
- return;
- }
-
- if (throwErrorCount > 1) {
- return;
- }
-
- servers.forEach(function (server) {
- if (server instanceof http.Server) {
- server.on('request', function (req, res) {
- // Let http server set `Connection: close` header, and close the current request socket.
- req.shouldKeepAlive = false;
- res.shouldKeepAlive = false;
- if (!res._header) {
- res.setHeader('Connection', 'close');
- }
- });
- }
- });
-
- // make sure we close down within `killTimeout` seconds
- var killtimer = setTimeout(function () {
- console.error('[%s] [graceful:worker:%s] kill timeout, exit now.', Date(), process.pid);
- if (process.env.NODE_ENV !== 'test') {
- // kill children by SIGKILL before exit
- killChildren(function() {
- process.exit(1);
- });
- }
- }, killTimeout);
- console.error('[%s] [graceful:worker:%s] will exit after %dms', Date(), process.pid, killTimeout);
-
- // But don't keep the process open just for that!
- // If there is no more io waitting, just let process exit normally.
- if (typeof killtimer.unref === 'function') {
- // only worked on node 0.10+
- killtimer.unref();
- }
-
- var worker = options.worker || cluster.worker;
-
- // cluster mode
- if (worker) {
- try {
- // stop taking new requests.
- // because server could already closed, need try catch the error: `Error: Not running`
- for (var i = 0; i < servers.length; i++) {
- var server = servers[i];
- server.close();
- console.error('[%s] [graceful:worker:%s] close server#%s, _connections: %s',
- Date(), process.pid, i, server._connections);
- }
- console.error('[%s] [graceful:worker:%s] close %d servers!',
- Date(), process.pid, servers.length);
- } catch (er1) {
- // Usually, this error throw cause by the active connections after the first domain error,
- // oh well, not much we can do at this point.
- console.error('[%s] [graceful:worker:%s] Error on server close!\n%s',
- Date(), process.pid, er1.stack);
- }
-
- try {
- // Let the master know we're dead. This will trigger a
- // 'disconnect' in the cluster master, and then it will fork
- // a new worker.
- worker.send('graceful:disconnect');
- worker.disconnect();
- console.error('[%s] [graceful:worker:%s] worker disconnect!',
- Date(), process.pid);
- } catch (er2) {
- // Usually, this error throw cause by the active connections after the first domain error,
- // oh well, not much we can do at this point.
- console.error('[%s] [graceful:worker:%s] Error on worker disconnect!\n%s',
- Date(), process.pid, er2.stack);
- }
- }
- });
-};
-
-function killChildren(callback) {
- pstree(process.pid, function(err, children) {
- if (err) {
- // if get children error, just ignore it
- console.error('[%s] [graceful:worker:%s] pstree find children error: %s', Date(), process.pid, err);
- callback();
- return;
- }
- children.forEach(function(child) {
- kill(parseInt(child.PID));
- });
- callback();
- });
-}
-
-function kill(pid) {
- try {
- process.kill(pid, 'SIGKILL');
- } catch (_) {
- // ignore
- }
-}
diff --git a/package.json b/package.json
index 3c78687..23ba23f 100644
--- a/package.json
+++ b/package.json
@@ -2,24 +2,6 @@
"name": "graceful",
"version": "1.1.0",
"description": "Graceful exit when `uncaughtException` emit, base on `process.on('uncaughtException')`.",
- "main": "index.js",
- "files": [
- "index.js"
- ],
- "scripts": {
- "test": "egg-bin test",
- "ci": "egg-bin cov",
- "lint": "echo 'ignore'"
- },
- "dependencies": {
- "humanize-ms": "^1.2.1",
- "ps-tree": "^1.1.0"
- },
- "devDependencies": {
- "egg-bin": "^5.6.1",
- "express": "^4.16.4",
- "supertest": "^1.2.0"
- },
"homepage": "https://github.com/node-modules/graceful",
"repository": {
"type": "git",
@@ -34,9 +16,65 @@
"cluster",
"graceful exit"
],
+ "author": "fengmk2 ",
+ "license": "MIT",
"engines": {
- "node": ">= 0.10.0"
+ "node": ">= 18.19.0"
},
- "author": "fengmk2 ",
- "license": "MIT"
+ "dependencies": {
+ "@fengmk2/ps-tree": "^2.0.2",
+ "humanize-ms": "^2.0.0"
+ },
+ "devDependencies": {
+ "@arethetypeswrong/cli": "^0.17.1",
+ "@eggjs/tsconfig": "1",
+ "@types/express": "^5.0.0",
+ "@types/mocha": "10",
+ "@types/node": "22",
+ "@types/supertest": "^6.0.2",
+ "egg-bin": "6",
+ "eslint": "8",
+ "eslint-config-egg": "14",
+ "express": "^4.21.2",
+ "mm": "^3.4.0",
+ "supertest": "^7.0.0",
+ "tshy": "3",
+ "tshy-after": "1",
+ "typescript": "5"
+ },
+ "scripts": {
+ "lint": "eslint --cache src test --ext .ts",
+ "pretest": "npm run lint -- --fix && npm run prepublishOnly",
+ "test": "egg-bin test",
+ "preci": "npm run lint && npm run prepublishOnly && attw --pack",
+ "ci": "egg-bin cov",
+ "prepublishOnly": "tshy && tshy-after"
+ },
+ "type": "module",
+ "tshy": {
+ "exports": {
+ ".": "./src/index.ts",
+ "./package.json": "./package.json"
+ }
+ },
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/esm/index.d.ts",
+ "default": "./dist/esm/index.js"
+ },
+ "require": {
+ "types": "./dist/commonjs/index.d.ts",
+ "default": "./dist/commonjs/index.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "types": "./dist/commonjs/index.d.ts",
+ "main": "./dist/commonjs/index.js",
+ "module": "./dist/esm/index.js"
}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..75d90f1
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,162 @@
+import { Server } from 'node:http';
+import cluster, { type Worker } from 'node:cluster';
+import { debuglog } from 'node:util';
+import { ms } from 'humanize-ms';
+import { pstree } from '@fengmk2/ps-tree';
+
+const debug = debuglog('graceful');
+
+export interface GracefulOptions {
+ /**
+ * servers, we need to close it and stop taking new requests.
+ */
+ servers?: Server[] | Server;
+ /**
+ * @deprecated please use `servers` instead
+ */
+ server?: Server[] | Server;
+ /**
+ * worker suicide timeout, default is 30 seconds
+ */
+ killTimeout?: number | string;
+ /**
+ * when uncaughtException emit, error(err, count).
+ * You can log error here.
+ */
+ error?: (err: Error, throwErrorCount: number) => void;
+ /**
+ * worker contains `disconnect()`
+ */
+ worker?: Worker;
+ /**
+ * ignore error code
+ */
+ ignoreCode?: number[];
+}
+
+/**
+ * graceful, please use with `cluster` in production env.
+ */
+export function graceful(options: GracefulOptions) {
+ const killTimeout = ms(options.killTimeout ?? '30s');
+ const onError = options.error || function() {};
+ let servers = options.servers ?? options.server ?? [];
+ const ignoreCode = options.ignoreCode ?? [];
+ if (!Array.isArray(servers)) {
+ servers = [ servers ];
+ }
+ if (servers.length === 0) {
+ throw new TypeError('options.servers required!');
+ }
+
+ let throwErrorCount = 0;
+ process.on('uncaughtException', err => {
+ throwErrorCount += 1;
+ onError(err, throwErrorCount);
+ console.error('[%s] [graceful:worker:%s:uncaughtException] throw error %d times',
+ Date(), process.pid, throwErrorCount);
+ console.error(err);
+ console.error(err.stack);
+ const errorCode = Reflect.get(err, 'code');
+ if (ignoreCode.includes(errorCode)) {
+ console.error('Error code: %s matches ignore list: %j, don\'t exit.', errorCode, ignoreCode);
+ return;
+ }
+
+ if (throwErrorCount > 1) {
+ return;
+ }
+
+ servers.forEach(server => {
+ if (server instanceof Server) {
+ server.on('request', (req, res) => {
+ // Let http server set `Connection: close` header, and close the current request socket.
+ // req.shouldKeepAlive = false;
+ Reflect.set(req, 'shouldKeepAlive', false);
+ res.shouldKeepAlive = false;
+ if (!res.headersSent) {
+ res.setHeader('Connection', 'close');
+ }
+ });
+ }
+ });
+
+ // make sure we close down within `killTimeout` seconds
+ const killTimer = setTimeout(async () => {
+ console.error('[%s] [graceful:worker:%s] kill timeout, exit now. NODE_ENV: %s',
+ Date(), process.pid, process.env.NODE_ENV);
+ if (process.env.NODE_ENV !== 'test') {
+ // kill children by SIGKILL before exit
+ await killChildren();
+ process.exit(1);
+ }
+ }, killTimeout);
+ console.error('[%s] [graceful:worker:%s] will exit after %dms',
+ Date(), process.pid, killTimeout);
+
+ // But don't keep the process open just for that!
+ // If there is no more io waiting, just let process exit normally.
+ killTimer.unref();
+
+ const worker = options.worker || cluster.worker;
+
+ // cluster mode
+ if (worker) {
+ try {
+ // stop taking new requests.
+ // because server could already closed, need try catch the error: `Error: Not running`
+ for (const [ i, server ] of servers.entries()) {
+ server.close();
+ console.error('[%s] [graceful:worker:%s] close server#%s, connections: %s',
+ Date(), process.pid, i, server.connections);
+ }
+ console.error('[%s] [graceful:worker:%s] close %d servers!',
+ Date(), process.pid, servers.length);
+ } catch (err: any) {
+ // Usually, this error throw cause by the active connections after the first domain error,
+ // oh well, not much we can do at this point.
+ console.error('[%s] [graceful:worker:%s] Error on server close!\n%s',
+ Date(), process.pid, err.stack);
+ }
+
+ try {
+ // Let the master know we're dead. This will trigger a
+ // 'disconnect' in the cluster master, and then it will fork
+ // a new worker.
+ worker.send('graceful:disconnect');
+ worker.disconnect();
+ console.error('[%s] [graceful:worker:%s] worker disconnect!',
+ Date(), process.pid);
+ } catch (err: any) {
+ // Usually, this error throw cause by the active connections after the first domain error,
+ // oh well, not much we can do at this point.
+ console.error('[%s] [graceful:worker:%s] Error on worker disconnect!\n%s',
+ Date(), process.pid, err.stack);
+ }
+ }
+ });
+}
+
+async function killChildren() {
+ try {
+ const children = await pstree(process.pid);
+ for (const child of children) {
+ killProcess(parseInt(child.PID));
+ }
+ console.error('[%s] [graceful:worker:%s] pstree find %d children and killed',
+ Date(), process.pid, children.length);
+ } catch (err) {
+ // if get children error, just ignore it
+ console.error('[%s] [graceful:worker:%s] pstree find children error: %s',
+ Date(), process.pid, err);
+ }
+}
+
+function killProcess(pid: number) {
+ try {
+ process.kill(pid, 'SIGKILL');
+ } catch (err) {
+ // ignore
+ debug('kill %s error: %s', pid, err);
+ }
+}
diff --git a/test/fixtures/app.cjs b/test/fixtures/app.cjs
new file mode 100644
index 0000000..4610995
--- /dev/null
+++ b/test/fixtures/app.cjs
@@ -0,0 +1,18 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const { fork } = require('node:child_process');
+const path = require('node:path');
+const { createServer } = require('node:http');
+const { graceful } = require('../../');
+
+fork(path.join(__dirname, 'worker.cjs'));
+
+const server = createServer();
+server.listen();
+graceful({
+ server,
+ killTimeout: 2000,
+});
+
+setTimeout(function() {
+ throw new Error('wow');
+}, 100);
diff --git a/test/fixtures/app.js b/test/fixtures/app.js
deleted file mode 100644
index ee27f42..0000000
--- a/test/fixtures/app.js
+++ /dev/null
@@ -1,19 +0,0 @@
-'use strict';
-
-var graceful = require('../../');
-var fork = require('child_process').fork;
-var path = require('path');
-
-fork(path.join(__dirname, 'worker.js'));
-
-
-var server = require('http').createServer();
-server.listen();
-graceful({
- server: server,
- killTimeout: 2000,
-});
-
-setTimeout(function() {
- throw new Error('wow');
-}, 100);
diff --git a/test/fixtures/foo.js b/test/fixtures/foo.js
index 65a8b38..8cc7aa3 100644
--- a/test/fixtures/foo.js
+++ b/test/fixtures/foo.js
@@ -1 +1 @@
-console.log('bar');
\ No newline at end of file
+console.log('bar');
diff --git a/test/fixtures/ignore.cjs b/test/fixtures/ignore.cjs
new file mode 100644
index 0000000..6decbad
--- /dev/null
+++ b/test/fixtures/ignore.cjs
@@ -0,0 +1,21 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const { fork } = require('node:child_process');
+const path = require('node:path');
+const { createServer } = require('node:http');
+const { graceful } = require('../../');
+
+fork(path.join(__dirname, 'worker.cjs'));
+
+const server = createServer();
+server.listen();
+graceful({
+ server,
+ killTimeout: 2000,
+ ignoreCode: [ 'EMOCKERROR' ],
+});
+
+setTimeout(function() {
+ const error = new Error('mock');
+ error.code = 'EMOCKERROR';
+ throw error;
+}, 1000);
diff --git a/test/fixtures/ignore.js b/test/fixtures/ignore.js
deleted file mode 100644
index 0810adf..0000000
--- a/test/fixtures/ignore.js
+++ /dev/null
@@ -1,22 +0,0 @@
-'use strict';
-
-var graceful = require('../../');
-var fork = require('child_process').fork;
-var path = require('path');
-
-fork(path.join(__dirname, 'worker.js'));
-
-
-var server = require('http').createServer();
-server.listen();
-graceful({
- server: server,
- killTimeout: 2000,
- ignoreCode: ['EMOCKERROR']
-});
-
-setTimeout(function () {
- const error = new Error('mock');
- error.code = 'EMOCKERROR';
- throw error;
-}, 1000);
diff --git a/test/fixtures/worker.js b/test/fixtures/worker.cjs
similarity index 94%
rename from test/fixtures/worker.js
rename to test/fixtures/worker.cjs
index d5fef0f..1593a02 100644
--- a/test/fixtures/worker.js
+++ b/test/fixtures/worker.cjs
@@ -1,5 +1,3 @@
-'use strict';
-
console.log('worker1 [%s] started', process.pid);
setTimeout(function() {
diff --git a/test/graceful.test.js b/test/graceful.test.js
deleted file mode 100644
index dc10702..0000000
--- a/test/graceful.test.js
+++ /dev/null
@@ -1,126 +0,0 @@
-'use strict';
-
-var cluster = require('cluster');
-var http = require('http');
-var request = require('supertest');
-var express = require('express');
-var graceful = require('../');
-
-describe('graceful.test.js', function () {
- function normalHandler(req, res, next) {
- if (req.url === '/sync_error') {
- throw new Error('sync_error');
- }
- if (req.url === '/async_error') {
- process.nextTick(function () {
- ff.foo();
- });
- return;
- }
- if (req.url === '/async_error_twice') {
- setTimeout(function () {
- ff.foo();
- }, 100);
- setTimeout(function () {
- bar.bar();
- }, 200);
- return;
- }
- if (req.url === '/async_error_triple') {
- setTimeout(function () {
- ff.foo();
- }, 100);
- setTimeout(function () {
- bar.bar();
- }, 200);
- setTimeout(function () {
- hehe.bar();
- }, 200);
- return;
- }
- res.end(req.url);
- }
-
- function errorHandler(err, req, res, next) {
- res.statusCode = 500;
- res.end(err.message);
- }
-
- var server = http.createServer();
- graceful({ server: server, killTimeout: '1s' });
-
- var app = express()
- .use('/public', express.static(__dirname + '/fixtures'))
- .use(normalHandler)
- .use(errorHandler);
-
- server.on('request', app);
-
- it('should GET / status 200', function (done) {
- request(server)
- .get('/')
- .expect(200, done);
- });
-
- it('should GET /public/foo.js status 200', function (done) {
- request(server)
- .get('/public/foo.js')
- .expect('console.log(\'bar\');')
- .expect(200, done);
- });
-
- it('should GET /sync_error status 500', function (done) {
- request(server)
- .get('/sync_error')
- .expect('sync_error')
- .expect(500, done);
- });
-
- describe.skip('hack for async error', function () {
- // Because `domain` will still throw `uncaughtException`, we need to hack for `mocha` test.
- // https://github.com/joyent/node/issues/4375
- // https://gist.github.com/4179636
- var mochaHandler;
- before(function () {
- mochaHandler = process.listeners('uncaughtException').pop();
- });
- after(function (done) {
- setTimeout(function () {
- // ...but be sure to re-enable mocha's error handler
- process.on('uncaughtException', mochaHandler);
- done();
- }, 2000);
- });
-
- beforeEach(function () {
- cluster.worker = {
- disconnect: function () {}
- };
- });
- afterEach(function () {
- delete cluster.worker;
- });
-
- it('should GET /async_error status 500', function (done) {
- delete cluster.worker.disconnect;
- request(server)
- .get('/async_error')
- .expect('ff is not defined')
- .expect(500, done);
- });
-
- it('should GET /async_error_twice status 500', function (done) {
- request(server)
- .get('/async_error_twice')
- .expect('ff is not defined')
- .expect(500, done);
- });
-
- it('should GET /async_error_triple status 500', function (done) {
- request(server)
- .get('/async_error_triple')
- .expect('ff is not defined')
- .expect(500, done);
- });
- });
-});
diff --git a/test/graceful.test.ts b/test/graceful.test.ts
new file mode 100644
index 0000000..c69c5dd
--- /dev/null
+++ b/test/graceful.test.ts
@@ -0,0 +1,87 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import http from 'node:http';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import request from 'supertest';
+import express from 'express';
+import { graceful } from '../src/index.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+describe('test/graceful.test.ts', () => {
+ function normalHandler(req: any, res: any) {
+ if (req.url === '/sync_error') {
+ throw new Error('sync_error');
+ }
+ if (req.url === '/async_error') {
+ process.nextTick(function() {
+ // @ts-ignore
+ ff.foo();
+ });
+ return;
+ }
+ if (req.url === '/async_error_twice') {
+ setTimeout(function() {
+ // @ts-ignore
+ ff.foo();
+ }, 100);
+ setTimeout(function() {
+ // @ts-ignore
+ bar.bar();
+ }, 200);
+ return;
+ }
+ if (req.url === '/async_error_triple') {
+ setTimeout(function() {
+ // @ts-ignore
+ ff.foo();
+ }, 100);
+ setTimeout(function() {
+ // @ts-ignore
+ bar.bar();
+ }, 200);
+ setTimeout(function() {
+ // @ts-ignore
+ hehe.bar();
+ }, 200);
+ return;
+ }
+ res.end(req.url);
+ }
+
+ function errorHandler(err: any, _req: any, res: any) {
+ res.statusCode = 500;
+ res.end(err.message);
+ }
+
+ const server = http.createServer();
+ graceful({ server, killTimeout: '1s' });
+
+ const app = express()
+ .use('/public', express.static(__dirname + '/fixtures'))
+ .use(normalHandler)
+ .use(errorHandler);
+
+ server.on('request', app);
+
+ it('should GET / status 200', function(done) {
+ request(server)
+ .get('/')
+ .expect(200, done);
+ });
+
+ it('should GET /public/foo.js status 200', function(done) {
+ request(server)
+ .get('/public/foo.js')
+ .expect('console.log(\'bar\');\n')
+ .expect(200, done);
+ });
+
+ it('should GET /sync_error status 500', function(done) {
+ request(server)
+ .get('/sync_error')
+ .expect(/sync_error/)
+ .expect(500, done);
+ });
+});
diff --git a/test/ignore.test.js b/test/ignore.test.js
deleted file mode 100644
index 5293f5c..0000000
--- a/test/ignore.test.js
+++ /dev/null
@@ -1,29 +0,0 @@
-'use strict';
-
-var assert = require('assert');
-var path = require('path');
-var fork = require('child_process').fork;
-
-
-describe('test/worker.test.js', () => {
- it('should kill all children', function (done) {
- var app = fork(path.join(__dirname, 'fixtures/ignore.js'));
- setTimeout(function() {
- assert(alive(app.pid));
- }, 1000);
-
- setTimeout(function () {
- assert(alive(app.pid));
- done();
- }, 4000);
- });
-});
-
-function alive(pid) {
- try {
- process.kill(pid, 0);
- return true;
- } catch (err) {
- return false;
- }
-}
diff --git a/test/ignore.test.ts b/test/ignore.test.ts
new file mode 100644
index 0000000..6cdd91d
--- /dev/null
+++ b/test/ignore.test.ts
@@ -0,0 +1,30 @@
+import { strict as assert } from 'node:assert';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { fork } from 'node:child_process';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+describe('test/ignore.test.ts', () => {
+ it('should kill all children', function(done) {
+ const app = fork(path.join(__dirname, 'fixtures/ignore.cjs'));
+ setTimeout(function() {
+ assert(alive(app.pid!));
+ }, 1000);
+
+ setTimeout(function() {
+ assert(alive(app.pid!));
+ done();
+ }, 4000);
+ });
+});
+
+function alive(pid: number) {
+ try {
+ process.kill(pid, 0);
+ return true;
+ } catch (err) {
+ return false;
+ }
+}
diff --git a/test/worker.test.js b/test/worker.test.js
deleted file mode 100644
index 2c6b590..0000000
--- a/test/worker.test.js
+++ /dev/null
@@ -1,36 +0,0 @@
-'use strict';
-
-var assert = require('assert');
-var path = require('path');
-var fork = require('child_process').fork;
-var pstree = require('ps-tree');
-
-
-describe('test/worker.test.js', () => {
- it('should kill all children', function (done) {
- var app = fork(path.join(__dirname, 'fixtures/app.js'));
- var workerPid;
- setTimeout(function() {
- assert(alive(app.pid));
- pstree(app.pid, function (_, children) {
- assert(children.length === 1);
- workerPid = children[0].PID;
- });
- }, 1000);
-
- setTimeout(function () {
- assert(!alive(app.pid));
- assert(!alive(workerPid));
- done();
- }, 4000);
- });
-});
-
-function alive(pid) {
- try {
- process.kill(pid, 0);
- return true;
- } catch (err) {
- return false;
- }
-}
diff --git a/test/worker.test.ts b/test/worker.test.ts
new file mode 100644
index 0000000..1c6a509
--- /dev/null
+++ b/test/worker.test.ts
@@ -0,0 +1,47 @@
+import assert from 'node:assert';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { fork } from 'node:child_process';
+import { pstree } from '@fengmk2/ps-tree';
+import mm from 'mm';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+describe('test/worker.test.ts', () => {
+ afterEach(mm.restore);
+
+ it('should kill all children', done => {
+ mm(process.env, 'NODE_ENV', 'prod');
+ const app = fork(path.join(__dirname, 'fixtures/app.cjs'));
+ let workerPid: string;
+ setTimeout(() => {
+ assert(alive(app.pid!), 'app.pid should alive');
+ pstree(app.pid!, (err, children) => {
+ if (err) {
+ return done(err);
+ }
+ assert(children);
+ assert.equal(children.length, 1);
+ workerPid = children[0].PID;
+ });
+ }, 1000);
+
+ setTimeout(() => {
+ assert(!alive(app.pid!), 'app.pid should not alive');
+ assert(!alive(Number(workerPid)), 'workerPid should not alive');
+ done();
+ }, 4000);
+ });
+});
+
+function alive(pid: number) {
+ try {
+ process.kill(pid, 0);
+ console.warn('%s alive', pid);
+ return true;
+ } catch (err) {
+ console.error('kill %s error: %s', pid, err);
+ return false;
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ff41b73
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "@eggjs/tsconfig",
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitAny": true,
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext"
+ }
+}