├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ ├── pkg.pr.new.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src └── index.ts ├── test ├── cluster-reload.test.ts ├── master.cjs └── worker.cjs └── 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, windows-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 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | pids 10 | logs 11 | results 12 | 13 | node_modules 14 | npm-debug.log 15 | .vscode 16 | .tshy* 17 | .eslintcache 18 | dist 19 | coverage 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.0](https://github.com/node-modules/cluster-reload/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 | ### Features 15 | 16 | * support cjs and esm both by tshy ([#8](https://github.com/node-modules/cluster-reload/issues/8)) ([a2981f8](https://github.com/node-modules/cluster-reload/commit/a2981f867398ad842216a42f05975bd87cb636c8)) 17 | 18 | 1.1.0 / 2022-11-17 19 | ================== 20 | 21 | **fixes** 22 | * [[`c9594bf`](http://github.com/node-modules/cluster-reload/commit/c9594bf5d66b6dbcb13c927d0d36888b1bf0b4ae)] - fix: 1.0.2 code (shaoshuai0102 <>) 23 | 24 | **others** 25 | * [[`a27942f`](http://github.com/node-modules/cluster-reload/commit/a27942f8bd907abc66930c6bee62f6509b52f308)] - 🤖 TEST: Run test on GitHub Action (#7) (fengmk2 <>) 26 | * [[`53acb67`](http://github.com/node-modules/cluster-reload/commit/53acb67a7356a642f6a9ea10a83ca134a630882a)] - chore: update https://registry.npm.taobao.org to https://registry.npmmirror.com (#4) (Non-Official NPM Mirror Bot <<99484857+npmmirror@users.noreply.github.com>>) 27 | 28 | 1.0.2 / 2015-12-23 29 | ================== 30 | 31 | * fix: Windows not support SIGQUIT, use SIGTERM instead 32 | 33 | 1.0.1 / 2015-06-30 34 | ================== 35 | 36 | * fix: make sure child worker kill itself 37 | 38 | 1.0.0 / 2015-06-03 39 | ================== 40 | 41 | * first commit 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2015 node-modules and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cluster-reload 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![npm download][download-image]][download-url] 5 | [![Node.js CI](https://github.com/node-modules/cluster-reload/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/cluster-reload/actions/workflows/nodejs.yml) 6 | [![Test coverage][codecov-image]][codecov-url] 7 | [![Node.js Version](https://img.shields.io/node/v/cluster-reload.svg?style=flat)](https://nodejs.org/en/download/) 8 | 9 | [npm-image]: https://img.shields.io/npm/v/cluster-reload.svg?style=flat-square 10 | [npm-url]: https://npmjs.org/package/cluster-reload 11 | [download-image]: https://img.shields.io/npm/dm/cluster-reload.svg?style=flat-square 12 | [download-url]: https://npmjs.org/package/cluster-reload 13 | [codecov-image]: https://codecov.io/github/node-modules/cluster-reload/coverage.svg?branch=master 14 | [codecov-url]: https://codecov.io/github/node-modules/cluster-reload?branch=master 15 | 16 | Easy and safe reload your workers. 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install cluster-reload 22 | ``` 23 | 24 | ## License 25 | 26 | [MIT](LICENSE) 27 | 28 | ## Contributors 29 | 30 | [![Contributors](https://contrib.rocks/image?repo=node-modules/cluster-reload)](https://github.com/node-modules/cluster-reload/graphs/contributors) 31 | 32 | Made with [contributors-img](https://contrib.rocks). 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster-reload", 3 | "version": "2.0.0", 4 | "description": "cluster workers reload", 5 | "homepage": "https://github.com/node-modules/cluster-reload", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/node-modules/cluster-reload.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/node-modules/cluster-reload/issues" 12 | }, 13 | "keywords": [ 14 | "cluster-reload" 15 | ], 16 | "author": "fengmk2 (https://github.com/fengmk2)", 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">= 18.19.0" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "@arethetypeswrong/cli": "^0.17.1", 24 | "@eggjs/tsconfig": "1", 25 | "@types/mocha": "10", 26 | "@types/node": "22", 27 | "egg-bin": "6", 28 | "eslint": "8", 29 | "eslint-config-egg": "14", 30 | "tshy": "3", 31 | "tshy-after": "1", 32 | "typescript": "5", 33 | "urllib": "^4.6.8" 34 | }, 35 | "scripts": { 36 | "lint": "eslint --cache src test --ext .ts", 37 | "pretest": "npm run lint -- --fix && npm run prepublishOnly", 38 | "test": "egg-bin test", 39 | "preci": "npm run lint && npm run prepublishOnly", 40 | "ci": "egg-bin cov && attw --pack", 41 | "prepublishOnly": "tshy && tshy-after" 42 | }, 43 | "type": "module", 44 | "tshy": { 45 | "exports": { 46 | ".": "./src/index.ts", 47 | "./package.json": "./package.json" 48 | } 49 | }, 50 | "exports": { 51 | ".": { 52 | "import": { 53 | "types": "./dist/esm/index.d.ts", 54 | "default": "./dist/esm/index.js" 55 | }, 56 | "require": { 57 | "types": "./dist/commonjs/index.d.ts", 58 | "default": "./dist/commonjs/index.js" 59 | } 60 | }, 61 | "./package.json": "./package.json" 62 | }, 63 | "files": [ 64 | "dist", 65 | "src" 66 | ], 67 | "types": "./dist/commonjs/index.d.ts", 68 | "main": "./dist/commonjs/index.js", 69 | "module": "./dist/esm/index.js" 70 | } 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import cluster, { type Worker } from 'node:cluster'; 2 | import { cpus } from 'node:os'; 3 | 4 | // Windows not support SIGQUIT https://nodejs.org/api/process.html#process_signal_events 5 | const KILL_SIGNAL = 'SIGTERM'; 6 | let reloading = false; 7 | let reloadPadding = false; 8 | 9 | export function reload(count?: number) { 10 | if (reloading) { 11 | reloadPadding = true; 12 | return; 13 | } 14 | if (!count) { 15 | count = cpus().length; 16 | } 17 | reloading = true; 18 | // find out all alive workers 19 | const aliveWorkers = []; 20 | for (const id in cluster.workers) { 21 | const worker = cluster.workers[id]!; 22 | const state = Reflect.get(worker, 'state'); 23 | if (state === 'disconnected') { 24 | continue; 25 | } 26 | aliveWorkers.push(worker); 27 | } 28 | 29 | let firstWorker: Worker; 30 | let newWorker: Worker; 31 | 32 | function reset() { 33 | // don't leak 34 | newWorker.removeListener('listening', reset); 35 | newWorker.removeListener('exit', reset); 36 | 37 | if (firstWorker) { 38 | // console.log('firstWorker %s %s', firstWorker.id, firstWorker.state); 39 | firstWorker.kill(KILL_SIGNAL); 40 | setTimeout(function() { 41 | firstWorker.process.kill(KILL_SIGNAL); 42 | }, 100); 43 | } 44 | reloading = false; 45 | if (reloadPadding) { 46 | // has reload jobs, reload again 47 | reloadPadding = false; 48 | reload(count); 49 | } 50 | } 51 | 52 | firstWorker = aliveWorkers[0]; 53 | newWorker = cluster.fork(); 54 | newWorker.on('listening', reset).on('exit', reset); 55 | 56 | // kill other workers 57 | for (const worker of aliveWorkers) { 58 | // console.log('worker %s %s', worker.id, worker.state); 59 | worker.kill(KILL_SIGNAL); 60 | } 61 | 62 | // keep workers number as before 63 | const left = count - 1; 64 | for (let j = 0; j < left; j++) { 65 | cluster.fork(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/cluster-reload.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { cpus } from 'node:os'; 3 | import { setTimeout as sleep } from 'node:timers/promises'; 4 | import urllib from 'urllib'; 5 | import { reload } from '../src/index.js'; 6 | 7 | const numCPUs = cpus().length; 8 | 9 | describe('test/cluster-reload.test.ts', () => { 10 | before(async () => { 11 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 12 | // @ts-ignore 13 | await import('./master.cjs'); 14 | await sleep(1000); 15 | }); 16 | 17 | after(async () => { 18 | await sleep(2000); 19 | }); 20 | 21 | it('should got 200', async () => { 22 | let success = false; 23 | while (!success) { 24 | try { 25 | const res = await urllib.request('http://127.0.0.1:7001'); 26 | assert.equal(res.data.toString(), 'hello world\n'); 27 | assert.equal(res.status, 200); 28 | success = true; 29 | } catch (err) { 30 | // console.error(err); 31 | await sleep(1000); 32 | } 33 | } 34 | }); 35 | 36 | it('should work with reloading', async () => { 37 | let res = await urllib.request('http://127.0.0.1:7001'); 38 | assert.equal(res.data.toString(), 'hello world\n'); 39 | assert.equal(res.status, 200); 40 | // console.log(res); 41 | reload(); 42 | let success = false; 43 | while (!success) { 44 | try { 45 | res = await urllib.request('http://127.0.0.1:7001'); 46 | assert.equal(res.data.toString(), 'hello world\n'); 47 | assert.equal(res.status, 200); 48 | success = true; 49 | } catch (err) { 50 | // console.error(err); 51 | await sleep(1000); 52 | } 53 | } 54 | }); 55 | 56 | it('should work with reload again', async () => { 57 | reload(numCPUs); 58 | reload(numCPUs); 59 | let success = false; 60 | while (!success) { 61 | try { 62 | const res = await urllib.request('http://127.0.0.1:7001'); 63 | assert.equal(res.data.toString(), 'hello world\n'); 64 | assert.equal(res.status, 200); 65 | success = true; 66 | } catch (err) { 67 | // console.error(err); 68 | await sleep(1000); 69 | } 70 | } 71 | await sleep(2000); 72 | }); 73 | 74 | it('should reload 1 workers still work', async () => { 75 | reload(1); 76 | let success = false; 77 | while (!success) { 78 | try { 79 | const res = await urllib.request('http://127.0.0.1:7001'); 80 | assert.equal(res.data.toString(), 'hello world\n'); 81 | assert.equal(res.status, 200); 82 | success = true; 83 | } catch (err) { 84 | // console.error(err); 85 | await sleep(1000); 86 | } 87 | } 88 | }); 89 | 90 | it('should exit and reload work', async () => { 91 | await assert.rejects(async () => { 92 | await urllib.request('http://127.0.0.1:7001/exit'); 93 | }, err => { 94 | assert(err); 95 | return true; 96 | }); 97 | 98 | reload(1); 99 | reload(1); 100 | reload(1); 101 | reload(1); 102 | await sleep(1000); 103 | 104 | let success = false; 105 | while (!success) { 106 | try { 107 | const res = await urllib.request('http://127.0.0.1:7001'); 108 | assert.equal(res.data.toString(), 'hello world\n'); 109 | assert.equal(res.status, 200); 110 | success = true; 111 | } catch (err) { 112 | // console.error(err); 113 | await sleep(1000); 114 | } 115 | } 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/master.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('node:path'); 3 | const cluster = require('node:cluster'); 4 | const numCPUs = require('node:os').cpus().length; 5 | 6 | cluster.setupMaster({ 7 | exec: path.join(__dirname, 'worker.cjs'), 8 | }); 9 | 10 | // Fork workers. 11 | for (let i = 0; i < numCPUs; i++) { 12 | cluster.fork(); 13 | } 14 | 15 | cluster.on('exit', function(worker, code, signal) { 16 | console.log('worker %s died, current workers: %j, code: %s, signal: %s', 17 | worker.id, Object.keys(cluster.workers), code, signal); 18 | }); 19 | console.log('master start'); 20 | -------------------------------------------------------------------------------- /test/worker.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const http = require('node:http'); 3 | 4 | http.createServer(function(req, res) { 5 | if (req.url === '/exit') { 6 | throw new Error('exit error'); 7 | } 8 | res.writeHead(200); 9 | res.end('hello world\n'); 10 | }).listen(7001, () => { 11 | console.log('worker %s listening at 7001', require('node:cluster').worker.id); 12 | }); 13 | console.log('worker %s start', require('node:cluster').worker.id); 14 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------