From 3d8d1169eed69a36997c2e53a3f61c2ebde79cc6 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sat, 26 Oct 2024 20:55:28 +0800 Subject: [PATCH 01/29] feat: support reusePort on server listen closes https://github.com/eggjs/egg/issues/5365 --- .github/PULL_REQUEST_TEMPLATE.md | 24 --------------- .github/workflows/nodejs.yml | 2 +- README.md | 12 +------- lib/app_worker.js | 20 ++++++++++--- lib/master.js | 1 + lib/utils/options.js | 1 + test/app_worker.test.js | 29 +++++++++++++++++++ .../fixtures/apps/app-listen-reusePort/app.js | 6 ++++ .../apps/app-listen-reusePort/app/router.js | 9 ++++++ .../config/config.default.js | 11 +++++++ .../apps/app-listen-reusePort/package.json | 3 ++ test/master.test.js | 11 +++++++ 12 files changed, 89 insertions(+), 40 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 test/fixtures/apps/app-listen-reusePort/app.js create mode 100644 test/fixtures/apps/app-listen-reusePort/app/router.js create mode 100644 test/fixtures/apps/app-listen-reusePort/config/config.default.js create mode 100644 test/fixtures/apps/app-listen-reusePort/package.json diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 48f9944..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ - - -##### Checklist - - -- [ ] `npm test` passes -- [ ] tests and/or benchmarks are included -- [ ] documentation is changed or added -- [ ] commit message follows commit guidelines - -##### Affected core subsystem(s) - - - -##### Description of change - diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index c8630f5..a7a80b8 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,6 +12,6 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest, macos-latest' - version: '14, 16, 18, 20, 22' + version: '14, 16, 18, 20, 22, 23' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index cd1bccc..47af651 100644 --- a/README.md +++ b/README.md @@ -71,16 +71,6 @@ EGG_AGENT_CLOSE_TIMEOUT: agent worker boot timeout value [MIT](LICENSE) - - ## Contributors -|[
popomore](https://github.com/popomore)
|[
fengmk2](https://github.com/fengmk2)
|[
atian25](https://github.com/atian25)
|[
dead-horse](https://github.com/dead-horse)
|[
killagu](https://github.com/killagu)
|[
semantic-release-bot](https://github.com/semantic-release-bot)
| -| :---: | :---: | :---: | :---: | :---: | :---: | -|[
ngot](https://github.com/ngot)
|[
hyj1991](https://github.com/hyj1991)
|[
whxaxes](https://github.com/whxaxes)
|[
iyuq](https://github.com/iyuq)
|[
nightink](https://github.com/nightink)
|[
mansonchor](https://github.com/mansonchor)
| -|[
ImHype](https://github.com/ImHype)
|[
gxcsoccer](https://github.com/gxcsoccer)
|[
waitingsong](https://github.com/waitingsong)
|[
sjfkai](https://github.com/sjfkai)
|[
ahungrynoob](https://github.com/ahungrynoob)
|[
qingdengyue](https://github.com/qingdengyue)
| -[
wenjiasen](https://github.com/wenjiasen)
|[
czy88840616](https://github.com/czy88840616)
|[
gxkl](https://github.com/gxkl)
- -This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Mon Jun 03 2024 10:59:15 GMT+0800`. - - +[![contributors](https://contrib.rocks/image?repo=eggjs/egg-cluster&max=240&columns=26)](https://github.com/eggjs/egg-cluster/graphs/contributors) diff --git a/lib/app_worker.js b/lib/app_worker.js index dabbbbc..26a32dd 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -35,6 +35,7 @@ const clusterConfig = app.config.cluster || /* istanbul ignore next */ {}; const listenConfig = clusterConfig.listen || /* istanbul ignore next */ {}; const httpsOptions = Object.assign({}, clusterConfig.https, options.https); const port = options.port = options.port || listenConfig.port; +const reusePort = options.reusePort = options.reusePort || listenConfig.reusePort; const debugPort = options.debugPort; const protocol = (httpsOptions.key && httpsOptions.cert) ? 'https' : 'http'; @@ -121,10 +122,21 @@ function startServer(err) { exitProcess(); return; } - const args = [ port ]; - if (listenConfig.hostname) args.push(listenConfig.hostname); - debug('listen options %s', args); - server.listen(...args); + if (reusePort) { + const listenOptions = { port, reusePort }; + if (listenConfig.hostname) { + listenOptions.host = listenConfig.hostname; + } + debug('listen options %s', listenOptions); + server.listen(listenOptions); + } else { + const args = [ port ]; + if (listenConfig.hostname) { + args.push(listenConfig.hostname); + } + debug('listen options %s', args); + server.listen(...args); + } } if (debugPortServer) { debug('listen on debug port: %s', debugPort); diff --git a/lib/master.js b/lib/master.js index 2dc1c20..e219835 100644 --- a/lib/master.js +++ b/lib/master.js @@ -34,6 +34,7 @@ class Master extends EventEmitter { * - {Object} [plugins] - customized plugins, for unittest * - {Number} [workers] numbers of app workers, default to `os.cpus().length` * - {Number} [port] listening port, default to 7001(http) or 8443(https) + * - {Boolean} [reusePort] setting `reusePort` to `true` allows multiple sockets on the same host to bind to the same port. Incoming connections are distributed by the operating system to listening sockets. This option is available only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. **Default:** `false`. * - {Number} [debugPort] listening a debug port on http protocol * - {Object} [https] https options, { key, cert, ca }, full path * - {Array|String} [require] will inject into worker/agent process diff --git a/lib/utils/options.js b/lib/utils/options.js index 3fff0f6..3103ea3 100644 --- a/lib/utils/options.js +++ b/lib/utils/options.js @@ -12,6 +12,7 @@ module.exports = function(options) { framework: '', baseDir: process.cwd(), port: options.https ? 8443 : null, + reusePort: false, workers: null, plugins: null, https: false, diff --git a/test/app_worker.test.js b/test/app_worker.test.js index 12db96b..d0b6529 100644 --- a/test/app_worker.test.js +++ b/test/app_worker.test.js @@ -232,6 +232,35 @@ describe('test/app_worker.test.js', () => { .expect(200); }); + it('should set reusePort=true in config', async () => { + app = utils.cluster('apps/app-listen-reusePort'); + // app.debug(); + await app.ready(); + + app.expect('code', 0); + app.expect('stdout', /egg started on http:\/\/127.0.0.1:17010/); + + await request('http://0.0.0.0:17010') + .get('/') + .expect('done') + .expect(200); + + await request('http://127.0.0.1:17010') + .get('/') + .expect('done') + .expect(200); + + await request('http://localhost:17010') + .get('/') + .expect('done') + .expect(200); + + await request('http://127.0.0.1:17010') + .get('/port') + .expect('17010') + .expect(200); + }); + it('should use hostname in config', async () => { const url = address.ip() + ':17010'; diff --git a/test/fixtures/apps/app-listen-reusePort/app.js b/test/fixtures/apps/app-listen-reusePort/app.js new file mode 100644 index 0000000..dc71e21 --- /dev/null +++ b/test/fixtures/apps/app-listen-reusePort/app.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = app => { + // don't use the port that egg-mock defined + app._options.port = undefined; +}; diff --git a/test/fixtures/apps/app-listen-reusePort/app/router.js b/test/fixtures/apps/app-listen-reusePort/app/router.js new file mode 100644 index 0000000..26e4658 --- /dev/null +++ b/test/fixtures/apps/app-listen-reusePort/app/router.js @@ -0,0 +1,9 @@ +module.exports = app => { + app.get('/', ctx => { + ctx.body = 'done'; + }); + + app.get('/port', ctx => { + ctx.body = ctx.app._options.port; + }); +}; diff --git a/test/fixtures/apps/app-listen-reusePort/config/config.default.js b/test/fixtures/apps/app-listen-reusePort/config/config.default.js new file mode 100644 index 0000000..afbe3d5 --- /dev/null +++ b/test/fixtures/apps/app-listen-reusePort/config/config.default.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + keys: '123', + cluster: { + listen: { + port: 17010, + reusePort: true, + }, + }, +}; diff --git a/test/fixtures/apps/app-listen-reusePort/package.json b/test/fixtures/apps/app-listen-reusePort/package.json new file mode 100644 index 0000000..f78929e --- /dev/null +++ b/test/fixtures/apps/app-listen-reusePort/package.json @@ -0,0 +1,3 @@ +{ + "name": "app-listen-reusePort" +} diff --git a/test/master.test.js b/test/master.test.js index abf32c6..b2a81ff 100644 --- a/test/master.test.js +++ b/test/master.test.js @@ -29,6 +29,17 @@ describe('test/master.test.js', () => { .end(done); }); + it('start success with reusePort=true', done => { + mm.env('local'); + app = utils.cluster('apps/master-worker-started', { reusePort: true }); + + app.expect('stdout', /egg start/) + .expect('stdout', /egg started/) + .notExpect('stdout', /\[master\] agent_worker#1:\d+ start with clusterPort:\d+/) + .expect('code', 0) + .end(done); + }); + it('start success in prod env', done => { mm.env('prod'); app = utils.cluster('apps/mock-production-app').debug(false); From dd4519421e8b1a1ecd27587ca6ab984ce6a0d12a Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sat, 26 Oct 2024 21:12:37 +0800 Subject: [PATCH 02/29] f --- test/app_worker.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app_worker.test.js b/test/app_worker.test.js index d0b6529..0e445f7 100644 --- a/test/app_worker.test.js +++ b/test/app_worker.test.js @@ -232,7 +232,7 @@ describe('test/app_worker.test.js', () => { .expect(200); }); - it('should set reusePort=true in config', async () => { + it.only('should set reusePort=true in config', async () => { app = utils.cluster('apps/app-listen-reusePort'); // app.debug(); await app.ready(); From cb897b67c878fb39a1c770ce0592bf0439e8dda6 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sat, 26 Oct 2024 21:38:31 +0800 Subject: [PATCH 03/29] f --- lib/agent_worker.js | 2 +- lib/app_worker.js | 11 ++++-- lib/utils/options.js | 1 - package.json | 2 +- test/reuseport_cluster.js | 70 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 test/reuseport_cluster.js diff --git a/lib/agent_worker.js b/lib/agent_worker.js index 678e70b..b1a0d58 100644 --- a/lib/agent_worker.js +++ b/lib/agent_worker.js @@ -24,7 +24,7 @@ if (options.startMode === 'worker_threads') { AgentWorker = require('./utils/mode/impl/process/agent').AgentWorker; } -const debug = require('util').debuglog('egg-cluster'); +const debug = require('util').debuglog('egg-cluster:agent_worker'); const ConsoleLogger = require('egg-logger').EggConsoleLogger; const consoleLogger = new ConsoleLogger({ level: process.env.EGG_AGENT_WORKER_LOGGER_LEVEL }); diff --git a/lib/app_worker.js b/lib/app_worker.js index 26a32dd..f6a7968 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -16,8 +16,9 @@ if (options.startMode === 'worker_threads') { AppWorker = require('./utils/mode/impl/process/app').AppWorker; } +const os = require('os'); const fs = require('fs'); -const debug = require('util').debuglog('egg-cluster'); +const debug = require('util').debuglog('egg-cluster:app_worker'); const ConsoleLogger = require('egg-logger').EggConsoleLogger; const consoleLogger = new ConsoleLogger({ level: process.env.EGG_APP_WORKER_LOGGER_LEVEL, @@ -35,10 +36,16 @@ const clusterConfig = app.config.cluster || /* istanbul ignore next */ {}; const listenConfig = clusterConfig.listen || /* istanbul ignore next */ {}; const httpsOptions = Object.assign({}, clusterConfig.https, options.https); const port = options.port = options.port || listenConfig.port; -const reusePort = options.reusePort = options.reusePort || listenConfig.reusePort; const debugPort = options.debugPort; const protocol = (httpsOptions.key && httpsOptions.cert) ? 'https' : 'http'; +let reusePort = options.reusePort = options.reusePort || listenConfig.reusePort; +if (reusePort && os.platform() !== 'linux') { + // Currently only linux is supported + reusePort = false; + debug('platform %s is not support currently, set reusePort to false', os.platform()); +} + AppWorker.send({ to: 'master', action: 'realport', diff --git a/lib/utils/options.js b/lib/utils/options.js index 3103ea3..2463d4c 100644 --- a/lib/utils/options.js +++ b/lib/utils/options.js @@ -69,7 +69,6 @@ module.exports = function(options) { const isDebug = process.execArgv.some(argv => argv.includes('--debug') || argv.includes('--inspect')); if (isDebug) options.isDebug = isDebug; - return options; }; diff --git a/package.json b/package.json index 02f807f..51e5521 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "npm run lint -- --fix && npm run test-local", "test-local": "egg-bin test --ts false", "cov": "egg-bin cov --prerequire --timeout 100000 --ts false", - "ci": "npm run lint && npm run cov", + "ci": "npm run lint && node test/reuseport_cluster.js && npm run cov", "contributor": "git-contributor" }, "files": [ diff --git a/test/reuseport_cluster.js b/test/reuseport_cluster.js new file mode 100644 index 0000000..be53f79 --- /dev/null +++ b/test/reuseport_cluster.js @@ -0,0 +1,70 @@ +const cluster = require('node:cluster'); +const http = require('node:http'); +const numCPUs = require('node:os').availableParallelism(); +const process = require('node:process'); + +function request(index) { + http.get('http://localhost:17001/', res => { + const { statusCode } = res; + console.log(index, res.statusCode, res.headers); + let error; + // Any 2xx status code signals a successful response but + // here we're only checking for 200. + if (statusCode !== 200) { + error = new Error('Request Failed.\n' + + `Status Code: ${statusCode}`); + } + if (error) { + console.error(error.message); + // Consume response data to free up memory + res.resume(); + return; + } + res.setEncoding('utf8'); + let rawData = ''; + res.on('data', chunk => { rawData += chunk; }); + res.on('end', () => { + try { + console.log(rawData); + } catch (e) { + console.error(e.message); + } + }); + }).on('error', e => { + console.error(`Got error: ${e.stack}`); + }); +} + +if (cluster.isPrimary) { + console.log(`Primary ${process.pid} is running`); + + // Fork workers. + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('exit', (worker, code, signal) => { + console.log(`worker ${worker.process.pid} died, code: ${code}, signal: ${signal}`); + }); + + setTimeout(() => { + for (let i = 0; i < 20; i++) { + request(i); + } + }, 2000); + setTimeout(() => { + process.exit(0); + }, 5000); +} else { + // Workers can share any TCP connection + // In this case it is an HTTP server + http.createServer((req, res) => { + res.writeHead(200); + res.end('hello world\n'); + }).listen({ + port: 17001, + reusePort: true, + }); + + console.log(`Worker ${process.pid} started`); +} From 0153ec5162a1b08740adb74549051a10afcc41eb Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Sat, 13 Dec 2025 00:13:07 +0800 Subject: [PATCH 04/29] Update test/app_worker.test.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: MK (fengmk2) --- test/app_worker.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app_worker.test.js b/test/app_worker.test.js index 0e445f7..d0b6529 100644 --- a/test/app_worker.test.js +++ b/test/app_worker.test.js @@ -232,7 +232,7 @@ describe('test/app_worker.test.js', () => { .expect(200); }); - it.only('should set reusePort=true in config', async () => { + it('should set reusePort=true in config', async () => { app = utils.cluster('apps/app-listen-reusePort'); // app.debug(); await app.ready(); From efdc99bb4d777c4a8d419300f2cd95b28aaced7b Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Sat, 13 Dec 2025 00:13:21 +0800 Subject: [PATCH 05/29] Update test/fixtures/apps/app-listen-reusePort/config/config.default.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: MK (fengmk2) --- .../fixtures/apps/app-listen-reusePort/config/config.default.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/fixtures/apps/app-listen-reusePort/config/config.default.js b/test/fixtures/apps/app-listen-reusePort/config/config.default.js index afbe3d5..eb4d18c 100644 --- a/test/fixtures/apps/app-listen-reusePort/config/config.default.js +++ b/test/fixtures/apps/app-listen-reusePort/config/config.default.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = { keys: '123', cluster: { From 8aea270bde4c13a800f64d3467ba123195929a0b Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 00:31:38 +0800 Subject: [PATCH 06/29] support reuse port on worker_threads --- .github/workflows/nodejs.yml | 2 +- .gitignore | 1 + README.md | 3 ++- lib/app_worker.js | 1 + lib/utils/mode/impl/worker_threads/app.js | 29 +++++++++++++++-------- package.json | 6 ++--- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 184e795..a957a0b 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,6 +12,6 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest, macos-latest' - version: '14, 16, 18, 20, 22, 23' + version: '14, 16, 18, 20, 22, 24' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 7f075b6..b15c3e4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ run .nyc_output package-lock.json .package-lock.json +pnpm-lock.yaml \ No newline at end of file diff --git a/README.md b/README.md index 208d9b0..3037d76 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,13 @@ startCluster(options, () => { | workers | `Number` | numbers of app workers | | sticky | `Boolean` | sticky mode server | | port | `Number` | port | +| reusePort | `Boolean` | (Required Node.js >= 22.12.0) allows multiple sockets on the same host to bind to the same port. Incoming connections are distributed by the operating system to listening sockets. This option is available only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. **Default:** `false` | | debugPort | `Number` | the debug port only listen on http protocol | | https | `Object` | start a https server, note: `key` / `cert` / `ca` should be full path to file | | require | `Array\|String` | will inject into worker/agent process | | pidFile | `String` | will save master pid to this file | | startMode | `String` | default is 'process', use 'worker_threads' to start the app & agent worker by worker_threads | -| ports | `Array` | startup port of each app worker, such as: [7001, 7002, 7003], only effects when the startMode is 'worker_threads' | +| ports | `Array` | startup port of each app worker, such as: [7001, 7002, 7003], only effects when the `startMode` is `'worker_threads'` and `reusePort` is `false` | | env | `String` | custom env, default is process.env.EGG_SERVER_ENV | ## Env diff --git a/lib/app_worker.js b/lib/app_worker.js index f6a7968..af84bbd 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -20,6 +20,7 @@ const os = require('os'); const fs = require('fs'); const debug = require('util').debuglog('egg-cluster:app_worker'); const ConsoleLogger = require('egg-logger').EggConsoleLogger; + const consoleLogger = new ConsoleLogger({ level: process.env.EGG_APP_WORKER_LOGGER_LEVEL, }); diff --git a/lib/utils/mode/impl/worker_threads/app.js b/lib/utils/mode/impl/worker_threads/app.js index bf6404e..2ef835d 100644 --- a/lib/utils/mode/impl/worker_threads/app.js +++ b/lib/utils/mode/impl/worker_threads/app.js @@ -155,17 +155,26 @@ class AppUtils extends BaseAppUtils { this.startTime = Date.now(); this.startSuccessCount = 0; - const ports = this.options.ports; - if (!ports.length) { - ports.push(this.options.port); + if (this.options.reusePort) { + for (let i = 0; i < this.options.workers; i++) { + const options = Object.assign({}, this.options, { port: ports[i] }); + const argv = [ JSON.stringify(options) ]; + const appWorkerId = i + 1; + this.#forkSingle(this.getAppWorkerFile(), { argv }, appWorkerId); + } + } else { + const ports = this.options.ports; + if (!ports.length) { + ports.push(this.options.port); + } + this.options.workers = ports.length; + let i = 0; + do { + const options = Object.assign({}, this.options, { port: ports[i] }); + const argv = [ JSON.stringify(options) ]; + this.#forkSingle(this.getAppWorkerFile(), { argv }, ++i); + } while (i < ports.length); } - this.options.workers = ports.length; - let i = 0; - do { - const options = Object.assign({}, this.options, { port: ports[i] }); - const argv = [ JSON.stringify(options) ]; - this.#forkSingle(this.getAppWorkerFile(), { argv }, ++i); - } while (i < ports.length); return this; } diff --git a/package.json b/package.json index 785ed47..ee3097d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/eggjs/egg-cluster.git" + "url": "git+https://github.com/eggjs/cluster.git" }, "keywords": [ "egg", @@ -26,9 +26,9 @@ "author": "dead-horse ", "license": "MIT", "bugs": { - "url": "https://github.com/eggjs/egg-cluster/issues" + "url": "https://github.com/eggjs/cluster/issues" }, - "homepage": "https://github.com/eggjs/egg-cluster#readme", + "homepage": "https://github.com/eggjs/cluster#readme", "dependencies": { "await-event": "^2.1.0", "cfork": "^1.7.1", From d1495cc592041b43bd1e74d5de1d67e27d5e8eda Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 00:34:13 +0800 Subject: [PATCH 07/29] FIXUP --- lib/app_worker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/app_worker.js b/lib/app_worker.js index af84bbd..36ffe8f 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -44,6 +44,7 @@ let reusePort = options.reusePort = options.reusePort || listenConfig.reusePort; if (reusePort && os.platform() !== 'linux') { // Currently only linux is supported reusePort = false; + options.reusePort = false; debug('platform %s is not support currently, set reusePort to false', os.platform()); } From 60f08cd7fae3a8fdf645d90f5dec2c8db84e03c0 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 00:52:04 +0800 Subject: [PATCH 08/29] FIXUP --- lib/utils/mode/impl/worker_threads/app.js | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/utils/mode/impl/worker_threads/app.js b/lib/utils/mode/impl/worker_threads/app.js index 2ef835d..e74099b 100644 --- a/lib/utils/mode/impl/worker_threads/app.js +++ b/lib/utils/mode/impl/worker_threads/app.js @@ -157,8 +157,7 @@ class AppUtils extends BaseAppUtils { if (this.options.reusePort) { for (let i = 0; i < this.options.workers; i++) { - const options = Object.assign({}, this.options, { port: ports[i] }); - const argv = [ JSON.stringify(options) ]; + const argv = [ JSON.stringify(this.options) ]; const appWorkerId = i + 1; this.#forkSingle(this.getAppWorkerFile(), { argv }, appWorkerId); } diff --git a/package.json b/package.json index ee3097d..f8bc603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "egg-cluster", - "version": "2.4.0", + "version": "2.5.0-beta.0", "description": "cluster manager for egg", "main": "index.js", "scripts": { From 16b546a309b5635f79dec2b876c1bff7d55c2fcd Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 01:31:47 +0800 Subject: [PATCH 09/29] FIXUP --- lib/utils/options.js | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/utils/options.js b/lib/utils/options.js index 2463d4c..9d5cf66 100644 --- a/lib/utils/options.js +++ b/lib/utils/options.js @@ -1,5 +1,6 @@ 'use strict'; +const cluster = require('cluster'); const os = require('os'); const fs = require('fs'); const path = require('path'); @@ -69,6 +70,18 @@ module.exports = function(options) { const isDebug = process.execArgv.some(argv => argv.includes('--debug') || argv.includes('--inspect')); if (isDebug) options.isDebug = isDebug; + + if (options.reusePort) { + if (os.platform() !== 'linux') { + // Currently only linux is supported + options.reusePort = false; + console.log('platform %s is not support currently, set reusePort to false', os.platform()); + } else { + // https://nodejs.org/api/cluster.html#clusterschedulingpolicy + // set cluster.schedulingPolicy to cluster.SCHED_NONE, leave it to the operating system + cluster.schedulingPolicy = cluster.SCHED_NONE; + } + } return options; }; diff --git a/package.json b/package.json index f8bc603..8ecae3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "egg-cluster", - "version": "2.5.0-beta.0", + "version": "2.5.0-beta.2", "description": "cluster manager for egg", "main": "index.js", "scripts": { From 9bd82dfcab8c15368f59ff761d5c278388413441 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 01:46:02 +0800 Subject: [PATCH 10/29] FIXUP --- lib/app_worker.js | 4 +++- lib/utils/options.js | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index 36ffe8f..f798852 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -132,7 +132,9 @@ function startServer(err) { return; } if (reusePort) { - const listenOptions = { port, reusePort }; + // https://nodejs.org/api/net.html#serverlistenoptions-callback + // When exclusive is true, the handle is not shared, and attempted port sharing results in an error. + const listenOptions = { port, reusePort, exclusive: true }; if (listenConfig.hostname) { listenOptions.host = listenConfig.hostname; } diff --git a/lib/utils/options.js b/lib/utils/options.js index 9d5cf66..e68a863 100644 --- a/lib/utils/options.js +++ b/lib/utils/options.js @@ -79,7 +79,7 @@ module.exports = function(options) { } else { // https://nodejs.org/api/cluster.html#clusterschedulingpolicy // set cluster.schedulingPolicy to cluster.SCHED_NONE, leave it to the operating system - cluster.schedulingPolicy = cluster.SCHED_NONE; + // cluster.schedulingPolicy = cluster.SCHED_NONE; } } return options; diff --git a/package.json b/package.json index 8ecae3b..fbe6fd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "egg-cluster", - "version": "2.5.0-beta.2", + "version": "2.5.0-beta.3", "description": "cluster manager for egg", "main": "index.js", "scripts": { From d1c0d2dcdea44e9f6d8799abbb56eec0f9697512 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Sat, 13 Dec 2025 10:39:31 +0800 Subject: [PATCH 11/29] Update lib/app_worker.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: MK (fengmk2) --- lib/app_worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index f798852..a6348a5 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -45,7 +45,7 @@ if (reusePort && os.platform() !== 'linux') { // Currently only linux is supported reusePort = false; options.reusePort = false; - debug('platform %s is not support currently, set reusePort to false', os.platform()); + debug('platform %s is not supported currently, set reusePort to false', os.platform()); } AppWorker.send({ From 6ef351c1c8c5a296dc5d3c43e7b876c8fb59bf43 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 11:07:42 +0800 Subject: [PATCH 12/29] cleanup on worker thread exit --- lib/app_worker.js | 3 +- lib/utils/mode/impl/worker_threads/app.js | 48 +++++++++++++++-------- lib/utils/options.js | 12 ------ 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index a6348a5..5dcb94b 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -133,8 +133,7 @@ function startServer(err) { } if (reusePort) { // https://nodejs.org/api/net.html#serverlistenoptions-callback - // When exclusive is true, the handle is not shared, and attempted port sharing results in an error. - const listenOptions = { port, reusePort, exclusive: true }; + const listenOptions = { port, reusePort }; if (listenConfig.hostname) { listenOptions.host = listenConfig.hostname; } diff --git a/lib/utils/mode/impl/worker_threads/app.js b/lib/utils/mode/impl/worker_threads/app.js index e74099b..ee03ba4 100644 --- a/lib/utils/mode/impl/worker_threads/app.js +++ b/lib/utils/mode/impl/worker_threads/app.js @@ -71,15 +71,14 @@ class AppWorker extends BaseAppWorker { } class AppUtils extends BaseAppUtils { - #workers = []; + #appWorkers = []; #forkSingle(appPath, options, id) { // start app worker const worker = new workerThreads.Worker(appPath, options); - this.#workers.push(worker); - // wrap app worker const appWorker = new AppWorker(worker, id); + this.#appWorkers.push(appWorker); this.emit('worker_forked', appWorker); appWorker.disableRefork = true; worker.on('message', msg => { @@ -129,23 +128,37 @@ class AppUtils extends BaseAppUtils { to: 'master', from: 'app', }); - }); // handle worker exit worker.on('exit', async code => { + this.log('[master] app_worker#%s (tid:%s) exit with code: %s', appWorker.id, appWorker.workerId, code); + // remove worker from workers array + this.#appWorkers.splice(this.#appWorkers.indexOf(appWorker), 1); + // remove all listeners to avoid memory leak + worker.removeAllListeners(); + appWorker.state = 'dead'; - this.messenger.send({ - action: 'app-exit', - data: { - workerId: appWorker.workerId, - code, - }, - to: 'master', - from: 'app', - }); + try { + this.messenger.send({ + action: 'app-exit', + data: { + workerId: appWorker.workerId, + code, + }, + to: 'master', + from: 'app', + }); + } catch (err) { + this.log('[master][warning] app_worker#%s (tid:%s) send "app-exit" message error: %s', + appWorker.id, appWorker.workerId, err); + } + if (appWorker.disableRefork) { + return; + } // refork app worker + this.log('[master] app_worker#%s (tid:%s) refork after 1s', appWorker.id, appWorker.workerId); await sleep(1000); this.#forkSingle(appPath, options, id); }); @@ -179,10 +192,11 @@ class AppUtils extends BaseAppUtils { } async kill() { - for (const worker of this.#workers) { - this.log(`[master] kill app worker#${worker.id} (worker_threads) by worker.terminate()`); - worker.removeAllListeners(); - worker.terminate(); + for (const appWorker of this.#appWorkers) { + this.log('[master] kill app_worker#%s (tid:%s) (worker_threads) by worker.terminate()', appWorker.id, appWorker.workerId); + appWorker.disableRefork = true; + appWorker.instance.removeAllListeners(); + appWorker.instance.terminate(); } } } diff --git a/lib/utils/options.js b/lib/utils/options.js index e68a863..3103ea3 100644 --- a/lib/utils/options.js +++ b/lib/utils/options.js @@ -1,6 +1,5 @@ 'use strict'; -const cluster = require('cluster'); const os = require('os'); const fs = require('fs'); const path = require('path'); @@ -71,17 +70,6 @@ module.exports = function(options) { const isDebug = process.execArgv.some(argv => argv.includes('--debug') || argv.includes('--inspect')); if (isDebug) options.isDebug = isDebug; - if (options.reusePort) { - if (os.platform() !== 'linux') { - // Currently only linux is supported - options.reusePort = false; - console.log('platform %s is not support currently, set reusePort to false', os.platform()); - } else { - // https://nodejs.org/api/cluster.html#clusterschedulingpolicy - // set cluster.schedulingPolicy to cluster.SCHED_NONE, leave it to the operating system - // cluster.schedulingPolicy = cluster.SCHED_NONE; - } - } return options; }; From 1c8b6fe88d93a0f312cb629cfead655995032b27 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 11:15:43 +0800 Subject: [PATCH 13/29] FIXUP --- lib/app_worker.js | 2 +- test/reuseport_cluster.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index 5dcb94b..9041c44 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -137,7 +137,7 @@ function startServer(err) { if (listenConfig.hostname) { listenOptions.host = listenConfig.hostname; } - debug('listen options %s', listenOptions); + debug('listen options %j', listenOptions); server.listen(listenOptions); } else { const args = [ port ]; diff --git a/test/reuseport_cluster.js b/test/reuseport_cluster.js index be53f79..d90d34e 100644 --- a/test/reuseport_cluster.js +++ b/test/reuseport_cluster.js @@ -1,8 +1,10 @@ const cluster = require('node:cluster'); const http = require('node:http'); -const numCPUs = require('node:os').availableParallelism(); +const os = require('node:os'); const process = require('node:process'); +const numCPUs = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length; + function request(index) { http.get('http://localhost:17001/', res => { const { statusCode } = res; @@ -60,7 +62,7 @@ if (cluster.isPrimary) { // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); - res.end('hello world\n'); + res.end(`hello world, worker pid: ${process.pid}, port: ${res.socket.localPort}\n`); }).listen({ port: 17001, reusePort: true, From 35bb21a28f41289bc3115a8adfa9bfc00e6248b4 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 11:20:43 +0800 Subject: [PATCH 14/29] FIXUP --- test/reuseport_cluster.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/reuseport_cluster.js b/test/reuseport_cluster.js index d90d34e..1f825f3 100644 --- a/test/reuseport_cluster.js +++ b/test/reuseport_cluster.js @@ -5,6 +5,8 @@ const process = require('node:process'); const numCPUs = typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length; +// pid: count +const totals = {}; function request(index) { http.get('http://localhost:17001/', res => { const { statusCode } = res; @@ -31,6 +33,8 @@ function request(index) { } catch (e) { console.error(e.message); } + const data = JSON.parse(rawData); + totals[data.pid] = (totals[data.pid] || 0) + 1; }); }).on('error', e => { console.error(`Got error: ${e.stack}`); @@ -50,11 +54,12 @@ if (cluster.isPrimary) { }); setTimeout(() => { - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 100; i++) { request(i); } }, 2000); setTimeout(() => { + console.log(totals); process.exit(0); }, 5000); } else { @@ -62,7 +67,7 @@ if (cluster.isPrimary) { // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); - res.end(`hello world, worker pid: ${process.pid}, port: ${res.socket.localPort}\n`); + res.end(JSON.stringify({ pid: process.pid })); }).listen({ port: 17001, reusePort: true, From cd94e19d487903d8949088a61786ab9c1183658e Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 11:28:53 +0800 Subject: [PATCH 15/29] FIXUP --- test/reuseport_cluster.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/reuseport_cluster.js b/test/reuseport_cluster.js index 1f825f3..d401ff9 100644 --- a/test/reuseport_cluster.js +++ b/test/reuseport_cluster.js @@ -8,7 +8,11 @@ const numCPUs = typeof os.availableParallelism === 'function' ? os.availablePara // pid: count const totals = {}; function request(index) { - http.get('http://localhost:17001/', res => { + http.get('http://localhost:17001/', { + headers: { + connection: 'close', + }, + }, res => { const { statusCode } = res; console.log(index, res.statusCode, res.headers); let error; @@ -54,7 +58,7 @@ if (cluster.isPrimary) { }); setTimeout(() => { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 1000; i++) { request(i); } }, 2000); @@ -70,7 +74,7 @@ if (cluster.isPrimary) { res.end(JSON.stringify({ pid: process.pid })); }).listen({ port: 17001, - reusePort: true, + reusePort: os.platform() === 'linux' ? true : false, }); console.log(`Worker ${process.pid} started`); From bd3c36a9512e49e869f6e23abc4beca778942931 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 11:30:38 +0800 Subject: [PATCH 16/29] FIXUP --- test/reuseport_cluster.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/reuseport_cluster.js b/test/reuseport_cluster.js index d401ff9..db2c472 100644 --- a/test/reuseport_cluster.js +++ b/test/reuseport_cluster.js @@ -74,7 +74,7 @@ if (cluster.isPrimary) { res.end(JSON.stringify({ pid: process.pid })); }).listen({ port: 17001, - reusePort: os.platform() === 'linux' ? true : false, + reusePort: os.platform() === 'linux', }); console.log(`Worker ${process.pid} started`); From 43841b7aad27e02346b7eff314c5cd63814b77ae Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 11:34:59 +0800 Subject: [PATCH 17/29] FIXUP --- test/reuseport_cluster.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/reuseport_cluster.js b/test/reuseport_cluster.js index db2c472..b947c51 100644 --- a/test/reuseport_cluster.js +++ b/test/reuseport_cluster.js @@ -45,7 +45,7 @@ function request(index) { }); } -if (cluster.isPrimary) { +if (cluster.isPrimary || cluster.isMaster) { console.log(`Primary ${process.pid} is running`); // Fork workers. From 6490b7593b242a154f0947620fd058e67fa2c66c Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Sat, 13 Dec 2025 11:44:52 +0800 Subject: [PATCH 18/29] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: MK (fengmk2) --- lib/utils/mode/impl/worker_threads/app.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/utils/mode/impl/worker_threads/app.js b/lib/utils/mode/impl/worker_threads/app.js index ee03ba4..e93bf86 100644 --- a/lib/utils/mode/impl/worker_threads/app.js +++ b/lib/utils/mode/impl/worker_threads/app.js @@ -134,7 +134,10 @@ class AppUtils extends BaseAppUtils { worker.on('exit', async code => { this.log('[master] app_worker#%s (tid:%s) exit with code: %s', appWorker.id, appWorker.workerId, code); // remove worker from workers array - this.#appWorkers.splice(this.#appWorkers.indexOf(appWorker), 1); + const idx = this.#appWorkers.indexOf(appWorker); + if (idx !== -1) { + this.#appWorkers.splice(idx, 1); + } // remove all listeners to avoid memory leak worker.removeAllListeners(); @@ -169,6 +172,9 @@ class AppUtils extends BaseAppUtils { this.startSuccessCount = 0; if (this.options.reusePort) { + if (!this.options.port) { + throw new Error('options.port must be specified when reusePort is enabled'); + } for (let i = 0; i < this.options.workers; i++) { const argv = [ JSON.stringify(this.options) ]; const appWorkerId = i + 1; From 6789f2b90d3a3dba11e5fe102c6ac610442fc2d1 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 13:37:21 +0800 Subject: [PATCH 19/29] FIXUP --- lib/app_worker.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index 9041c44..a83aef6 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -40,9 +40,13 @@ const port = options.port = options.port || listenConfig.port; const debugPort = options.debugPort; const protocol = (httpsOptions.key && httpsOptions.cert) ? 'https' : 'http'; +// https://nodejs.org/api/net.html#serverlistenoptions-callback +// https://github.com/nodejs/node/blob/main/node.gypi#L310 +// https://docs.python.org/3/library/sys.html#sys.platform +// This option is available only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. +const supportedPlatforms = ['linux', 'freebsd', 'sunos', 'aix']; let reusePort = options.reusePort = options.reusePort || listenConfig.reusePort; -if (reusePort && os.platform() !== 'linux') { - // Currently only linux is supported +if (reusePort && !supportedPlatforms.includes(os.platform())) { reusePort = false; options.reusePort = false; debug('platform %s is not supported currently, set reusePort to false', os.platform()); From fe60af082788403584b3a32c2b6b4433e2acef8c Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 13:40:02 +0800 Subject: [PATCH 20/29] FIXUP --- lib/app_worker.js | 2 +- test/app_worker.test.js | 4 ++-- test/master.test.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index a83aef6..d557118 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -44,7 +44,7 @@ const protocol = (httpsOptions.key && httpsOptions.cert) ? 'https' : 'http'; // https://github.com/nodejs/node/blob/main/node.gypi#L310 // https://docs.python.org/3/library/sys.html#sys.platform // This option is available only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. -const supportedPlatforms = ['linux', 'freebsd', 'sunos', 'aix']; +const supportedPlatforms = [ 'linux', 'freebsd', 'sunos', 'aix' ]; let reusePort = options.reusePort = options.reusePort || listenConfig.reusePort; if (reusePort && !supportedPlatforms.includes(os.platform())) { reusePort = false; diff --git a/test/app_worker.test.js b/test/app_worker.test.js index d0b6529..e6d6247 100644 --- a/test/app_worker.test.js +++ b/test/app_worker.test.js @@ -232,9 +232,9 @@ describe('test/app_worker.test.js', () => { .expect(200); }); - it('should set reusePort=true in config', async () => { + it.only('should set reusePort=true in config', async () => { app = utils.cluster('apps/app-listen-reusePort'); - // app.debug(); + app.debug(); await app.ready(); app.expect('code', 0); diff --git a/test/master.test.js b/test/master.test.js index b2a81ff..ff06543 100644 --- a/test/master.test.js +++ b/test/master.test.js @@ -29,9 +29,10 @@ describe('test/master.test.js', () => { .end(done); }); - it('start success with reusePort=true', done => { + it.only('start success with reusePort=true', done => { mm.env('local'); app = utils.cluster('apps/master-worker-started', { reusePort: true }); + app.debug(); app.expect('stdout', /egg start/) .expect('stdout', /egg started/) From ca5d801d1aa735e51d5be9e246b3d41e33aeca98 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 13:46:59 +0800 Subject: [PATCH 21/29] FIXUP --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fbe6fd4..81d37a2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint": "eslint .", "test": "npm run lint -- --fix && npm run test-local", "test-local": "egg-bin test --ts false", - "cov": "egg-bin cov --prerequire --timeout 100000 --ts false", + "cov": "NODE_DEBUG=egg-cluster:app_worker egg-bin cov --prerequire --timeout 100000 --ts false", "ci": "npm run lint && node test/reuseport_cluster.js && npm run cov" }, "files": [ From 545a4426398195a8549bf3ac787ff530d25365c2 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 13:57:27 +0800 Subject: [PATCH 22/29] FIXUP --- lib/utils/mode/impl/process/app.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/mode/impl/process/app.js b/lib/utils/mode/impl/process/app.js index 61b8a2e..ff20338 100644 --- a/lib/utils/mode/impl/process/app.js +++ b/lib/utils/mode/impl/process/app.js @@ -124,6 +124,7 @@ class AppUtils extends BaseAppUtils { }); cluster.on('listening', (worker, address) => { const appWorker = new AppWorker(worker); + this.logger.info('[master] app_worker#%s:%s listening on %j', appWorker.id, appWorker.workerId, address); this.messenger.send({ action: 'app-start', data: { diff --git a/package.json b/package.json index 81d37a2..ca43f58 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint": "eslint .", "test": "npm run lint -- --fix && npm run test-local", "test-local": "egg-bin test --ts false", - "cov": "NODE_DEBUG=egg-cluster:app_worker egg-bin cov --prerequire --timeout 100000 --ts false", + "cov": "NODE_DEBUG=egg-cluster* egg-bin cov --prerequire --timeout 100000 --ts false", "ci": "npm run lint && node test/reuseport_cluster.js && npm run cov" }, "files": [ From 0da63a6ae468caf540cd55ec99af4d0b5b05fd79 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:02:25 +0800 Subject: [PATCH 23/29] FIXUP --- lib/app_worker.js | 1 + lib/utils/mode/impl/process/app.js | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index d557118..c92968a 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -162,6 +162,7 @@ function startServer(err) { to: 'master', action: 'listening', data: server.address() || { port }, + reusePort, }); } diff --git a/lib/utils/mode/impl/process/app.js b/lib/utils/mode/impl/process/app.js index ff20338..eda0174 100644 --- a/lib/utils/mode/impl/process/app.js +++ b/lib/utils/mode/impl/process/app.js @@ -41,8 +41,12 @@ class AppWorker extends BaseAppWorker { process.on(event, callback); } - static send(data) { - process.send(data); + static send(message) { + if (message && message.action === 'listening' && message.reusePort) { + cluster.worker.send(message); + return; + } + process.send(message); } static kill() { @@ -122,6 +126,7 @@ class AppUtils extends BaseAppUtils { from: 'app', }); }); + // won't get listening event when reusePort is true, use `message` instead cluster.on('listening', (worker, address) => { const appWorker = new AppWorker(worker); this.logger.info('[master] app_worker#%s:%s listening on %j', appWorker.id, appWorker.workerId, address); @@ -135,6 +140,23 @@ class AppUtils extends BaseAppUtils { from: 'app', }); }); + // handle 'reuse-port-listening' message: { action: 'reuse-port-listening', data: { port: 3000 } } + cluster.on('message', (worker, message) => { + if (!message || message.action !== 'reuse-port-listening') { + return; + } + const appWorker = new AppWorker(worker); + this.logger.info('[master] app_worker#%s:%s reuse-port listening on %j', appWorker.id, appWorker.workerId, message); + this.messenger.send({ + action: 'app-start', + data: { + workerId: appWorker.workerId, + address: message.data, + }, + to: 'master', + from: 'app', + }); + }); return this; } From 4a3369721609b7a33c68e879e28ea8a6894cdbef Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:06:18 +0800 Subject: [PATCH 24/29] FIXUP --- lib/utils/mode/impl/process/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/mode/impl/process/app.js b/lib/utils/mode/impl/process/app.js index eda0174..2c2133e 100644 --- a/lib/utils/mode/impl/process/app.js +++ b/lib/utils/mode/impl/process/app.js @@ -42,7 +42,9 @@ class AppWorker extends BaseAppWorker { } static send(message) { + console.log('send message', message); if (message && message.action === 'listening' && message.reusePort) { + // cluster won't get `listening` event when reusePort is true, use cluster `message` event instead cluster.worker.send(message); return; } @@ -126,7 +128,6 @@ class AppUtils extends BaseAppUtils { from: 'app', }); }); - // won't get listening event when reusePort is true, use `message` instead cluster.on('listening', (worker, address) => { const appWorker = new AppWorker(worker); this.logger.info('[master] app_worker#%s:%s listening on %j', appWorker.id, appWorker.workerId, address); From 144b5592483bf81f1c5fa5913eb0f87b041bde5a Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:07:32 +0800 Subject: [PATCH 25/29] FIXUP --- lib/utils/mode/impl/process/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/mode/impl/process/app.js b/lib/utils/mode/impl/process/app.js index 2c2133e..4ab95aa 100644 --- a/lib/utils/mode/impl/process/app.js +++ b/lib/utils/mode/impl/process/app.js @@ -42,9 +42,10 @@ class AppWorker extends BaseAppWorker { } static send(message) { - console.log('send message', message); if (message && message.action === 'listening' && message.reusePort) { // cluster won't get `listening` event when reusePort is true, use cluster `message` event instead + // rewrite message.action to 'reuse-port-listening' + message.action = 'reuse-port-listening'; cluster.worker.send(message); return; } From cf121f1d6a9c1c0110df82aefb3097f8a63786c6 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:19:26 +0800 Subject: [PATCH 26/29] FIXUP --- lib/app_worker.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/app_worker.js b/lib/app_worker.js index c92968a..6cfd821 100644 --- a/lib/app_worker.js +++ b/lib/app_worker.js @@ -158,11 +158,13 @@ function startServer(err) { } } - AppWorker.send({ - to: 'master', - action: 'listening', - data: server.address() || { port }, - reusePort, + server.once('listening', () => { + AppWorker.send({ + to: 'master', + action: 'listening', + data: server.address() || { port }, + reusePort, + }); }); } From 2b065f98d7f546a810ccd1e27ad4b0df5b053900 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:25:31 +0800 Subject: [PATCH 27/29] FIXUP --- lib/utils/mode/impl/process/app.js | 5 +++-- package.json | 2 +- test/app_worker.test.js | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/utils/mode/impl/process/app.js b/lib/utils/mode/impl/process/app.js index 4ab95aa..8e8f2cc 100644 --- a/lib/utils/mode/impl/process/app.js +++ b/lib/utils/mode/impl/process/app.js @@ -147,13 +147,14 @@ class AppUtils extends BaseAppUtils { if (!message || message.action !== 'reuse-port-listening') { return; } + const address = message.data; const appWorker = new AppWorker(worker); - this.logger.info('[master] app_worker#%s:%s reuse-port listening on %j', appWorker.id, appWorker.workerId, message); + this.logger.info('[master] app_worker#%s:%s reuse-port listening on %j', appWorker.id, appWorker.workerId, address); this.messenger.send({ action: 'app-start', data: { workerId: appWorker.workerId, - address: message.data, + address, }, to: 'master', from: 'app', diff --git a/package.json b/package.json index ca43f58..fbe6fd4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint": "eslint .", "test": "npm run lint -- --fix && npm run test-local", "test-local": "egg-bin test --ts false", - "cov": "NODE_DEBUG=egg-cluster* egg-bin cov --prerequire --timeout 100000 --ts false", + "cov": "egg-bin cov --prerequire --timeout 100000 --ts false", "ci": "npm run lint && node test/reuseport_cluster.js && npm run cov" }, "files": [ diff --git a/test/app_worker.test.js b/test/app_worker.test.js index e6d6247..6c48805 100644 --- a/test/app_worker.test.js +++ b/test/app_worker.test.js @@ -238,7 +238,8 @@ describe('test/app_worker.test.js', () => { await app.ready(); app.expect('code', 0); - app.expect('stdout', /egg started on http:\/\/127.0.0.1:17010/); + // IPv6 first: http://:::17010 + app.expect('stdout', /egg started on http:\/\/.+?:17010/); await request('http://0.0.0.0:17010') .get('/') From 063012c21bd9b44fe516184562e0b0f98260f848 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:29:57 +0800 Subject: [PATCH 28/29] FIXUP --- test/master.test.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/master.test.js b/test/master.test.js index ff06543..52200c2 100644 --- a/test/master.test.js +++ b/test/master.test.js @@ -43,7 +43,24 @@ describe('test/master.test.js', () => { it('start success in prod env', done => { mm.env('prod'); - app = utils.cluster('apps/mock-production-app').debug(false); + app = utils.cluster('apps/mock-production-app') + .debug(false); + + app.expect('stdout', /egg start/) + .expect('stdout', /egg started/) + .expect('code', 0) + .end(err => { + assert.ifError(err); + console.log(app.stdout); + console.log(app.stderr); + done(); + }); + }); + + it.only('start success with reusePort=true in prod env', done => { + mm.env('prod'); + app = utils.cluster('apps/mock-production-app', { reusePort: true, workers: 4 }) + .debug(); app.expect('stdout', /egg start/) .expect('stdout', /egg started/) From 9e99e6fed67712392d2b8a647b36adc8f3f5b2b1 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 13 Dec 2025 16:32:25 +0800 Subject: [PATCH 29/29] FIXUP --- test/app_worker.test.js | 2 +- test/master.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/app_worker.test.js b/test/app_worker.test.js index 6c48805..ec312e1 100644 --- a/test/app_worker.test.js +++ b/test/app_worker.test.js @@ -232,7 +232,7 @@ describe('test/app_worker.test.js', () => { .expect(200); }); - it.only('should set reusePort=true in config', async () => { + it('should set reusePort=true in config', async () => { app = utils.cluster('apps/app-listen-reusePort'); app.debug(); await app.ready(); diff --git a/test/master.test.js b/test/master.test.js index 52200c2..1bcdc93 100644 --- a/test/master.test.js +++ b/test/master.test.js @@ -29,7 +29,7 @@ describe('test/master.test.js', () => { .end(done); }); - it.only('start success with reusePort=true', done => { + it('start success with reusePort=true', done => { mm.env('local'); app = utils.cluster('apps/master-worker-started', { reusePort: true }); app.debug(); @@ -57,7 +57,7 @@ describe('test/master.test.js', () => { }); }); - it.only('start success with reusePort=true in prod env', done => { + it('start success with reusePort=true in prod env', done => { mm.env('prod'); app = utils.cluster('apps/mock-production-app', { reusePort: true, workers: 4 }) .debug();