-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(cluster): support reusePort on server listen #5754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| import fs from 'node:fs'; | ||
| import { createServer as createHttpServer, type Server } from 'node:http'; | ||
| import { createServer as createHttpsServer } from 'node:https'; | ||
| import type { Socket } from 'node:net'; | ||
| import type { Socket, ListenOptions } from 'node:net'; | ||
| import os from 'node:os'; | ||
| import { debuglog } from 'node:util'; | ||
|
|
||
| import { importModule } from '@eggjs/utils'; | ||
|
|
@@ -13,6 +14,12 @@ import { AppThreadWorker } from './utils/mode/impl/worker_threads/app.ts'; | |
|
|
||
| const debug = debuglog('egg/cluster/app_worker'); | ||
|
|
||
| // 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 REUSE_PORT_SUPPORTED_PLATFORMS = ['linux', 'freebsd', 'sunos', 'aix']; | ||
|
Comment on lines
+17
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n packages/cluster/src/app_worker.ts | head -30Repository: eggjs/egg Length of output: 1560 🌐 Web query:
💡 Result: Node.js supports the server.listen({ reusePort: true }) option only on some OSes — specifically: Linux (kernel ≥ 3.9), DragonFlyBSD (≥ 3.6), FreeBSD (≥ 12.0), Solaris 11.4, and AIX 7.2.5+. The reusePort option was added in Node v22.12.0 / v23.1.0. Windows is not listed as supported. [1][2] Sources:
🏁 Script executed: # Check for usages of REUSE_PORT_SUPPORTED_PLATFORMS
rg "REUSE_PORT_SUPPORTED_PLATFORMS" packages/cluster/src/Repository: eggjs/egg Length of output: 279 🌐 Web query:
💡 Result: Node.js does not return "dragonfly". os.platform() / process.platform only return one of: 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', 'win32' (and sometimes 'android'). DragonFly BSD is not listed as a distinct value. [1] Sources:
Remove DragonFlyBSD reference from comment or add 'dragonfly' to supported platforms array. The comment mentions "DragonFlyBSD 3.6+" as a supported platform where SO_REUSEPORT is available, but the Either remove the DragonFlyBSD reference from the comment since it cannot be detected, or verify if there's an alternative detection mechanism for DragonFlyBSD systems. 🤖 Prompt for AI Agents |
||
|
|
||
| async function main() { | ||
| // $ node app_worker.js options-json-string | ||
| const options = JSON.parse(process.argv[2]) as { | ||
|
|
@@ -25,6 +32,7 @@ async function main() { | |
| https?: object; | ||
| sticky?: boolean; | ||
| stickyWorkerPort?: number; | ||
| reusePort?: boolean; | ||
| }; | ||
| if (options.require) { | ||
| // inject | ||
|
|
@@ -89,13 +97,26 @@ async function main() { | |
| const port = (app.options.port = options.port || listenConfig.port); | ||
| const debugPort = options.debugPort; | ||
| const protocol = httpsOptions.key && httpsOptions.cert ? 'https' : 'http'; | ||
|
|
||
| // Check reusePort option and validate platform support | ||
| let reusePort = options.reusePort ?? listenConfig.reusePort ?? false; | ||
| if (reusePort && !REUSE_PORT_SUPPORTED_PLATFORMS.includes(os.platform())) { | ||
| reusePort = false; | ||
| debug( | ||
| '[app_worker:%s] platform %s is not supported for reusePort, set reusePort to false', | ||
| process.pid, | ||
| os.platform(), | ||
| ); | ||
| } | ||
|
|
||
| debug( | ||
| '[app_worker:%s] listenConfig: %j, real port: %o, protocol: %o, debugPort: %o', | ||
| '[app_worker:%s] listenConfig: %j, real port: %o, protocol: %o, debugPort: %o, reusePort: %o', | ||
| process.pid, | ||
| listenConfig, | ||
| port, | ||
| protocol, | ||
| debugPort, | ||
| reusePort, | ||
| ); | ||
|
|
||
| AppWorker.send({ | ||
|
|
@@ -158,12 +179,23 @@ async function main() { | |
| exitProcess(); | ||
| return; | ||
| } | ||
| const args = [port]; | ||
| if (listenConfig.hostname) { | ||
| args.push(listenConfig.hostname); | ||
| if (reusePort) { | ||
| // https://nodejs.org/api/net.html#serverlistenoptions-callback | ||
| // Use options object when reusePort is enabled | ||
| const listenOptions: ListenOptions = { port, reusePort }; | ||
| if (listenConfig.hostname) { | ||
| listenOptions.host = listenConfig.hostname; | ||
| } | ||
| debug('[app_worker:%s] listen with reusePort options %j', process.pid, listenOptions); | ||
| server.listen(listenOptions); | ||
| } else { | ||
| const args = [port]; | ||
| if (listenConfig.hostname) { | ||
| args.push(listenConfig.hostname); | ||
| } | ||
| debug('listen options %j', args); | ||
| server.listen(...args); | ||
| } | ||
| debug('listen options %j', args); | ||
| server.listen(...args); | ||
| } | ||
| if (debugPortServer) { | ||
| debug('listen on debug port: %s', debugPort); | ||
|
|
@@ -181,14 +213,15 @@ async function main() { | |
| addressType: -1, | ||
| }; | ||
| } | ||
| debug('[app_worker:%s] listening at %j', process.pid, address); | ||
| debug('[app_worker:%s] listening at %j, reusePort: %o', process.pid, address, reusePort); | ||
| AppWorker.send({ | ||
| to: 'master', | ||
| action: 'app-start', | ||
| data: { | ||
| address, | ||
| workerId: AppWorker.workerId, | ||
| }, | ||
| reusePort, | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,17 +140,30 @@ export class AppThreadUtils 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) { | ||
| // When reusePort is enabled, all workers share the same port | ||
| // and each worker has its own socket | ||
| 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)]; | ||
| this.#forkSingle(this.getAppWorkerFile(), { argv }, i + 1); | ||
| } | ||
| } else { | ||
| // Normal mode: each worker can have a different port | ||
| 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); | ||
|
Comment on lines
+160
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The for (let i = 0; i < ports.length; i++) {
const options = { ...this.options, port: ports[i] };
const argv = [JSON.stringify(options)];
this.#forkSingle(this.getAppWorkerFile(), { argv }, i + 1);
}
Comment on lines
+153
to
+165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, let's check the target file and surrounding context
echo "=== Checking target file: packages/cluster/src/utils/mode/impl/worker_threads/app.ts ==="
if [ -f "packages/cluster/src/utils/mode/impl/worker_threads/app.ts" ]; then
wc -l packages/cluster/src/utils/mode/impl/worker_threads/app.ts
echo "--- Lines 150-170 for context ---"
sed -n '150,170p' packages/cluster/src/utils/mode/impl/worker_threads/app.ts
else
echo "File not found, searching for it..."
find . -name "app.ts" -path "*/worker_threads/*" 2>/dev/null
fiRepository: eggjs/egg Length of output: 886 🏁 Script executed: #!/bin/bash
# Search for parseOptions function and how port is handled
echo "=== Searching for parseOptions function ==="
rg -n "parseOptions" --type=ts | head -20Repository: eggjs/egg Length of output: 1859 🏁 Script executed: #!/bin/bash
# Look for port initialization in Master class
echo "=== Checking Master class for port initialization ==="
rg -n "this.options.port" packages/cluster/src/ --type=ts -A 2 -B 2 | head -50Repository: eggjs/egg Length of output: 1727 🏁 Script executed: #!/bin/bash
# Search for tests related to worker_threads mode
echo "=== Checking tests with worker_threads ==="
fd -e test.ts -e spec.ts packages/cluster | head -10Repository: eggjs/egg Length of output: 482 🏁 Script executed: #!/bin/bash
# Check the parseOptions implementation
echo "=== Checking parseOptions implementation ==="
sed -n '100,200p' packages/cluster/src/utils/options.tsRepository: eggjs/egg Length of output: 2754 🏁 Script executed: #!/bin/bash
# Check the ParsedClusterOptions type definition
echo "=== Checking type definitions ==="
rg -n "interface ParsedClusterOptions|type ParsedClusterOptions" --type=ts -A 20 packages/cluster/src/utils/Repository: eggjs/egg Length of output: 1542 🏁 Script executed: #!/bin/bash
# Look at test cases for parseOptions
echo "=== Checking parseOptions tests ==="
cat packages/cluster/test/options.test.tsRepository: eggjs/egg Length of output: 8577 Add a guard to ensure The code assumes Either add a check like reusePort mode does, or provide a sensible default port (e.g., 7001). 🤖 Prompt for AI Agents |
||
| } | ||
| 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; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -78,6 +78,12 @@ export interface ClusterOptions { | |||||
| * sticky mode server | ||||||
| */ | ||||||
| sticky?: boolean; | ||||||
| /** | ||||||
| * enable SO_REUSEPORT socket option for server listen, default is `false`. | ||||||
| * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. | ||||||
|
||||||
| * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. | |
| * Only available on Linux 3.9+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| module.exports = (app) => { | ||
| // don't use the port that egg-mock defined | ||
| app._options.port = undefined; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| module.exports = (app) => { | ||
| app.get('/', (ctx) => { | ||
| ctx.body = 'done'; | ||
| }); | ||
|
|
||
| app.get('/port', (ctx) => { | ||
| ctx.body = ctx.app.options.port; | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| module.exports = { | ||
| keys: '123', | ||
| cluster: { | ||
| listen: { | ||
| port: 17010, | ||
| reusePort: true, | ||
| }, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "name": "app-listen-reusePort" | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -359,12 +359,15 @@ const factory: EggConfigFactory = defineConfigFactory((appInfo): PartialEggConfi | |||||
| * @property {String} listen.path - set a unix sock path when server listen | ||||||
| * @property {Number} listen.port - set a port when server listen | ||||||
| * @property {String} listen.hostname - set a hostname binding server when server listen | ||||||
| * @property {Boolean} listen.reusePort - enable SO_REUSEPORT socket option, default is `false`. | ||||||
| * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. | ||||||
|
||||||
| * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. | |
| * Only available on Linux 3.9+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -258,6 +258,12 @@ export interface EggAppConfig extends EggCoreAppConfig { | |||||
| path: string; | ||||||
| port: number; | ||||||
| hostname: string; | ||||||
| /** | ||||||
| * enable SO_REUSEPORT socket option for server listen, default is `false`. | ||||||
| * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. | ||||||
|
||||||
| * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. | |
| * Only available on Linux 3.9+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment mentions DragonFlyBSD 3.6+ as a supported platform, but the REUSE_PORT_SUPPORTED_PLATFORMS array doesn't include 'dragonflybsd'. According to Node.js os.platform() documentation, DragonFlyBSD would likely return 'dragonfly' or similar, not 'freebsd'. Either update the comment to remove DragonFlyBSD from the list of supported platforms, or add the appropriate platform identifier to the array if DragonFlyBSD support is intended.