Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions packages/cluster/src/app_worker.ts
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';
Expand All @@ -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+.
Copy link

Copilot AI Dec 30, 2025

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.

Suggested change
// 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+.
// This option is available only on some platforms, such as Linux 3.9+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+.

Copilot uses AI. Check for mistakes.
const REUSE_PORT_SUPPORTED_PLATFORMS = ['linux', 'freebsd', 'sunos', 'aix'];
Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/cluster/src/app_worker.ts | head -30

Repository: eggjs/egg

Length of output: 1560


🌐 Web query:

Node.js SO_REUSEPORT platform support documentation server.listen() supported platforms

💡 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:

  • Node.js server.listen docs (reusePort supported platforms + history). [1]
  • Node.js server.listen (v25 API page showing reusePort text & history). [2]

🏁 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:

Node.js os.platform() return value for DragonFlyBSD

💡 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:

  • Node.js os.platform() / process.platform documentation. [1]

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 REUSE_PORT_SUPPORTED_PLATFORMS array doesn't include any value for it. Since os.platform() returns 'freebsd' for FreeBSD but does not return 'dragonfly' for DragonFlyBSD (DragonFlyBSD is not a documented return value from os.platform()), DragonFlyBSD systems cannot be detected by the current check, making the comment inaccurate.

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
In packages/cluster/src/app_worker.ts around lines 17 to 21, the comment
references "DragonFlyBSD" but the REUSE_PORT_SUPPORTED_PLATFORMS array does not
include a matching platform string and os.platform() does not reliably return a
DragonFlyBSD identifier; update the file by either removing the DragonFlyBSD
mention from the comment to keep it accurate, or if you want to support
DragonFlyBSD, add the correct platform string (confirm the exact os.platform()
value for DragonFlyBSD) to REUSE_PORT_SUPPORTED_PLATFORMS and add a unit/test or
runtime check to ensure detection works cross-platform.


async function main() {
// $ node app_worker.js options-json-string
const options = JSON.parse(process.argv[2]) as {
Expand All @@ -25,6 +32,7 @@ async function main() {
https?: object;
sticky?: boolean;
stickyWorkerPort?: number;
reusePort?: boolean;
};
if (options.require) {
// inject
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);
Expand All @@ -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,
});
});
}
Expand Down
6 changes: 6 additions & 0 deletions packages/cluster/src/utils/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export interface MessageBody {
receiverPid?: string;
receiverWorkerId?: string;
senderWorkerId?: string;
/**
* Whether reusePort is enabled for server listen.
* When reusePort is true, cluster won't get `listening` event,
* so we need to use cluster `message` event instead.
*/
reusePort?: boolean;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/cluster/src/utils/mode/impl/process/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cluster, { type Worker as ClusterProcessWorker } from 'node:cluster';
import { debuglog } from 'node:util';

import { cfork } from 'cfork';
import { graceful as gracefulExit, type Options as gracefulExitOptions } from 'graceful-process';
Expand All @@ -8,6 +9,8 @@ import type { MessageBody } from '../../../messenger.ts';
import { terminate } from '../../../terminate.ts';
import { BaseAppWorker, BaseAppUtils } from '../../base/app.ts';

const debug = debuglog('egg/cluster/utils/mode/impl/process/app');

export class AppProcessWorker extends BaseAppWorker<ClusterProcessWorker> {
get id(): number {
return this.instance.id;
Expand Down Expand Up @@ -45,6 +48,13 @@ export class AppProcessWorker extends BaseAppWorker<ClusterProcessWorker> {

static send(message: MessageBody): void {
message.senderWorkerId = String(process.pid);
// cluster won't get `listening` event when reusePort is true,
// use cluster `message` event instead
if (message.action === 'app-start' && message.reusePort) {
debug('send app-start message with reusePort, use cluster.worker.send()');
cluster.worker!.send(message);
return;
}
process.send!(message);
}

Expand Down
33 changes: 23 additions & 10 deletions packages/cluster/src/utils/mode/impl/worker_threads/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The do-while loop for forking workers in non-reusePort mode is a bit unconventional and less readable than a standard for loop. Refactoring this to use a for loop and modern object spread syntax would improve code clarity and maintainability, making it more consistent with the reusePort logic branch.

      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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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
fi

Repository: 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 -20

Repository: 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 -50

Repository: 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 -10

Repository: 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.ts

Repository: 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.ts

Repository: eggjs/egg

Length of output: 8577


Add a guard to ensure options.port is defined in normal mode, or use a fallback port.

The code assumes options.port is defined at line 157, but parseOptions explicitly defaults it to undefined unless HTTPS is enabled (see packages/cluster/src/utils/options.ts:103). Test cases confirm port can be undefined by default. This is inconsistent with reusePort mode, which validates port with an explicit error at lines 146-148. If port is undefined, the ports array receives undefined, and workers get port: undefined in their options, which will cause issues.

Either add a check like reusePort mode does, or provide a sensible default port (e.g., 7001).

🤖 Prompt for AI Agents
In packages/cluster/src/utils/mode/impl/worker_threads/app.ts around lines 153
to 165, the code can push an undefined port into the ports array when
this.options.port is undefined; update the block so that when ports is empty it
uses a validated port value (either throw a clear error like the reusePort
branch or set a sensible fallback, e.g., const defaultPort = 7001) and push that
instead of undefined, ensure the pushed value is a defined number before
assigning this.options.workers and forking workers so each worker receives a
concrete port in its options.

}
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;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/cluster/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Copy link

Copilot AI Dec 30, 2025

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 this doesn't match the platform validation logic in app_worker.ts. The REUSE_PORT_SUPPORTED_PLATFORMS array doesn't include a DragonFlyBSD platform identifier. Either remove DragonFlyBSD from the documentation or ensure the implementation supports it.

Suggested change
* 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+.

Copilot uses AI. Check for mistakes.
* @see https://nodejs.org/api/net.html#serverlistenoptions-callback
*/
reusePort?: boolean;
/** customized plugins, for unittest */
plugins?: object;
isDebug?: boolean;
Expand Down
24 changes: 24 additions & 0 deletions packages/cluster/test/app_worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,30 @@ describe.skipIf(process.version.startsWith('v24') || process.platform === 'win32
const sock = encodeURIComponent(sockFile);
await request(`http+unix://${sock}`).get('/').expect('done').expect(200);
});

it.skipIf(process.platform !== 'linux')('should use reusePort in config on Linux', async () => {
app = cluster('apps/app-listen-reusePort', { port: 0, workers: 2 });
// app.debug();
await app.ready();

app.expect('code', 0);
app.expect('stdout', /egg started on http:\/\/127.0.0.1:17010/);

await request('http://127.0.0.1:17010').get('/').expect('done').expect(200);
await request('http://127.0.0.1:17010').get('/port').expect('17010').expect(200);
});

it('should set reusePort=true in config (non-Linux will fallback to false)', async () => {
app = cluster('apps/app-listen-reusePort', { port: 0 });
// app.debug();
await app.ready();

app.expect('code', 0);
app.expect('stdout', /egg started on http:\/\/127.0.0.1:17010/);

await request('http://127.0.0.1:17010').get('/').expect('done').expect(200);
await request('http://127.0.0.1:17010').get('/port').expect('17010').expect(200);
});
});

it('should exit when EADDRINUSE', async () => {
Expand Down
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"
}
3 changes: 3 additions & 0 deletions packages/egg/src/config/config.default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Copy link

Copilot AI Dec 30, 2025

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 this doesn't match the platform validation logic in app_worker.ts. The REUSE_PORT_SUPPORTED_PLATFORMS array doesn't include a DragonFlyBSD platform identifier. Either remove DragonFlyBSD from the documentation or ensure the implementation supports it.

Suggested change
* 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+.

Copilot uses AI. Check for mistakes.
*/
config.cluster = {
listen: {
path: '',
port: 7001,
hostname: '',
reusePort: false,
},
};

Expand Down
6 changes: 6 additions & 0 deletions packages/egg/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Copy link

Copilot AI Dec 30, 2025

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 this doesn't match the platform validation logic in app_worker.ts. The REUSE_PORT_SUPPORTED_PLATFORMS array doesn't include a DragonFlyBSD platform identifier. Either remove DragonFlyBSD from the documentation or ensure the implementation supports it.

Suggested change
* 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+.

Copilot uses AI. Check for mistakes.
* @see https://nodejs.org/api/net.html#serverlistenoptions-callback
*/
reusePort?: boolean;
};
};

Expand Down
Loading