Skip to content
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

feat: support reusePort on server listen #115

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
24 changes: 0 additions & 24 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,6 @@ EGG_AGENT_CLOSE_TIMEOUT: agent worker boot timeout value

[MIT](LICENSE)

<!-- GITCONTRIBUTOR_START -->

## Contributors

|[<img src="https://avatars.githubusercontent.com/u/360661?v=4" width="100px;"/><br/><sub><b>popomore</b></sub>](https://github.com/popomore)<br/>|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|[<img src="https://avatars.githubusercontent.com/u/6897780?v=4" width="100px;"/><br/><sub><b>killagu</b></sub>](https://github.com/killagu)<br/>|[<img src="https://avatars.githubusercontent.com/u/32174276?v=4" width="100px;"/><br/><sub><b>semantic-release-bot</b></sub>](https://github.com/semantic-release-bot)<br/>|
| :---: | :---: | :---: | :---: | :---: | :---: |
|[<img src="https://avatars.githubusercontent.com/u/5243774?v=4" width="100px;"/><br/><sub><b>ngot</b></sub>](https://github.com/ngot)<br/>|[<img src="https://avatars.githubusercontent.com/u/19908330?v=4" width="100px;"/><br/><sub><b>hyj1991</b></sub>](https://github.com/hyj1991)<br/>|[<img src="https://avatars.githubusercontent.com/u/5856440?v=4" width="100px;"/><br/><sub><b>whxaxes</b></sub>](https://github.com/whxaxes)<br/>|[<img src="https://avatars.githubusercontent.com/u/2170848?v=4" width="100px;"/><br/><sub><b>iyuq</b></sub>](https://github.com/iyuq)<br/>|[<img src="https://avatars.githubusercontent.com/u/2972143?v=4" width="100px;"/><br/><sub><b>nightink</b></sub>](https://github.com/nightink)<br/>|[<img src="https://avatars.githubusercontent.com/u/2160731?v=4" width="100px;"/><br/><sub><b>mansonchor</b></sub>](https://github.com/mansonchor)<br/>|
|[<img src="https://avatars.githubusercontent.com/u/10825163?v=4" width="100px;"/><br/><sub><b>ImHype</b></sub>](https://github.com/ImHype)<br/>|[<img src="https://avatars.githubusercontent.com/u/1207064?v=4" width="100px;"/><br/><sub><b>gxcsoccer</b></sub>](https://github.com/gxcsoccer)<br/>|[<img src="https://avatars.githubusercontent.com/u/1763067?v=4" width="100px;"/><br/><sub><b>waitingsong</b></sub>](https://github.com/waitingsong)<br/>|[<img src="https://avatars.githubusercontent.com/u/7581901?v=4" width="100px;"/><br/><sub><b>sjfkai</b></sub>](https://github.com/sjfkai)<br/>|[<img src="https://avatars.githubusercontent.com/u/26563778?v=4" width="100px;"/><br/><sub><b>ahungrynoob</b></sub>](https://github.com/ahungrynoob)<br/>|[<img src="https://avatars.githubusercontent.com/u/3230673?v=4" width="100px;"/><br/><sub><b>qingdengyue</b></sub>](https://github.com/qingdengyue)<br/>|
[<img src="https://avatars.githubusercontent.com/u/16320597?v=4" width="100px;"/><br/><sub><b>wenjiasen</b></sub>](https://github.com/wenjiasen)<br/>|[<img src="https://avatars.githubusercontent.com/u/418820?v=4" width="100px;"/><br/><sub><b>czy88840616</b></sub>](https://github.com/czy88840616)<br/>|[<img src="https://avatars.githubusercontent.com/u/9213756?v=4" width="100px;"/><br/><sub><b>gxkl</b></sub>](https://github.com/gxkl)<br/>

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`.

<!-- GITCONTRIBUTOR_END -->
[![contributors](https://contrib.rocks/image?repo=eggjs/egg-cluster&max=240&columns=26)](https://github.com/eggjs/egg-cluster/graphs/contributors)
2 changes: 1 addition & 1 deletion lib/agent_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down
29 changes: 24 additions & 5 deletions lib/app_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
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,
Expand All @@ -38,6 +39,13 @@
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',
Expand Down Expand Up @@ -121,10 +129,21 @@
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 };
Comment on lines +132 to +133
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add type validation for reusePort option.

While port validation exists (line 127-131), there's no validation for the reusePort option. Consider adding type checking to ensure robust error handling.

 if (reusePort) {
+  if (typeof reusePort !== 'boolean') {
+    consoleLogger.error('[app_worker] reusePort should be boolean, but got %s(%s)', reusePort, typeof reusePort);
+    exitProcess();
+    return;
+  }
   const listenOptions = { port, reusePort };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (reusePort) {
const listenOptions = { port, reusePort };
if (reusePort) {
if (typeof reusePort !== 'boolean') {
consoleLogger.error('[app_worker] reusePort should be boolean, but got %s(%s)', reusePort, typeof reusePort);
exitProcess();
return;
}
const listenOptions = { port, reusePort };

if (listenConfig.hostname) {
listenOptions.host = listenConfig.hostname;
}

Check warning on line 136 in lib/app_worker.js

View check run for this annotation

Codecov / codecov/patch

lib/app_worker.js#L135-L136

Added lines #L135 - L136 were not covered by tests
debug('listen options %s', listenOptions);
server.listen(listenOptions);
} else {
Comment on lines +132 to +139
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding validation for reusePort option.

While port validation exists, there's no validation for the reusePort option. Consider adding type checking to ensure robust error handling.

 if (reusePort) {
+  if (typeof reusePort !== 'boolean') {
+    consoleLogger.error('[app_worker] reusePort should be boolean, but got %s(%s)', reusePort, typeof reusePort);
+    exitProcess();
+    return;
+  }
   const listenOptions = { port, reusePort };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (reusePort) {
const listenOptions = { port, reusePort };
if (listenConfig.hostname) {
listenOptions.host = listenConfig.hostname;
}
debug('listen options %s', listenOptions);
server.listen(listenOptions);
} else {
if (reusePort) {
if (typeof reusePort !== 'boolean') {
consoleLogger.error('[app_worker] reusePort should be boolean, but got %s(%s)', reusePort, typeof reusePort);
exitProcess();
return;
}
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);
}

Check warning on line 143 in lib/app_worker.js

View check run for this annotation

Codecov / codecov/patch

lib/app_worker.js#L142-L143

Added lines #L142 - L143 were not covered by tests
debug('listen options %s', args);
server.listen(...args);
}
Comment on lines +132 to +146
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Additional test coverage needed for reusePort scenarios

The current test suite only verifies basic functionality of reusePort with a single worker. Missing test coverage for:

  • Multiple workers binding to the same port with reusePort enabled
  • Error handling when some workers have reusePort enabled and others don't
  • Load balancing behavior across multiple workers with reusePort

Suggested test additions:

  • Test with multiple workers (2+) all using reusePort
  • Test error scenarios with mixed reusePort configurations
  • Test connection distribution across workers
🔗 Analysis chain

Verify reusePort behavior under different scenarios.

The implementation looks good and aligns with the PR objectives. Consider testing these scenarios:

  1. Multiple workers binding to the same port
  2. Performance impact with and without reusePort
  3. Behavior when some workers have reusePort enabled and others don't

Let me check the test coverage in detail to verify if all scenarios are properly tested.


Let me check if there are tests covering the multiple workers scenario and error cases.


Let me check the actual test implementation for the reusePort configuration to see how it's being tested with multiple workers.


Let me check the test fixture implementation to understand how reusePort is configured.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check test coverage for reusePort scenarios
rg -l "reusePort.*true" test/

Length of output: 140


Script:

#!/bin/bash
# Check test implementations for reusePort
rg -A 10 "reusePort.*true" test/

Length of output: 1663


Script:

#!/bin/bash
# Check for worker-related tests with reusePort
rg -B 5 -A 15 "workers.*=.*[2-9]" test/

Length of output: 2283


Script:

#!/bin/bash
# Check the full test implementation for reusePort
cat test/app_worker.test.js

Length of output: 9691


Script:

#!/bin/bash
# Check the test fixture implementation
cat test/fixtures/apps/app-listen-reusePort/config/config.default.js

Length of output: 203

}
if (debugPortServer) {
debug('listen on debug port: %s', debugPort);
Expand Down
1 change: 1 addition & 0 deletions lib/master.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -68,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;
};

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider integrating reuseport test into the main test suite.

While adding the test to CI is good, running it directly with Node bypasses the test framework. Consider moving this test into the main test suite to:

  • Ensure consistent test reporting
  • Include results in coverage metrics
  • Leverage the test framework's features

Apply this diff to integrate with the test framework:

-    "ci": "npm run lint && node test/reuseport_cluster.js && npm run cov",
+    "ci": "npm run lint && npm run test-local && npm run cov",

Then move the reuseport cluster test logic into a proper test file that integrates with your test framework.

Committable suggestion was skipped due to low confidence.

"contributor": "git-contributor"
},
"files": [
Expand Down
29 changes: 29 additions & 0 deletions test/app_worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,35 @@
.expect(200);
});

it.only('should set reusePort=true in config', async () => {

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 16)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 16)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 18)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 18)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 14)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 14)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 20)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 20)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 22)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 22)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 14)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 14)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 23)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (macos-latest, 23)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 16)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 16)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 18)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 18)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 22)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 22)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 23)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 23)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 20)

it.only not permitted

Check warning on line 235 in test/app_worker.test.js

View workflow job for this annotation

GitHub Actions / Node.js / Test (ubuntu-latest, 20)

it.only not permitted
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove .only from the test case.

The use of it.only prevents other tests from running. This is typically used during development for debugging purposes but should be removed before merging.

Apply this diff to fix the issue:

-    it.only('should set reusePort=true in config', async () => {
+    it('should set reusePort=true in config', async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.only('should set reusePort=true in config', async () => {
it('should set reusePort=true in config', async () => {
🧰 Tools
🪛 GitHub Check: Node.js / Test (ubuntu-latest, 23)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 22)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 23)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 20)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 18)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 16)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 14)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 22)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 20)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 16)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 14)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 18)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 Biome

[error] 235-235: Don't focus the test.

The 'only' method is often used for debugging or during implementation. It should be removed before deploying to production.
Consider removing 'only' to ensure all tests are executed.
Unsafe fix: Remove focus from test.

(lint/suspicious/noFocusedTests)

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);
});
Comment on lines +235 to +262
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage for reusePort functionality.

While the test verifies basic HTTP functionality, it doesn't specifically verify that the SO_REUSEPORT option is properly set. Consider adding assertions to validate the socket option.

Here's a suggested enhancement to verify reusePort is actually set:

     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/);
+      // Verify reusePort is set in the worker process
+      await app.httpRequest()
+        .get('/get-socket-options')
+        .expect(res => {
+          assert(res.body.reusePort === true, 'Expected SO_REUSEPORT to be enabled');
+        })
+        .expect(200);
 
       await request('http://0.0.0.0:17010')
         .get('/')

This requires adding a new endpoint in your test fixture that exposes the socket options. I can help implement this if needed.

Would you like me to provide the implementation for the socket options endpoint in the test fixture?

Committable suggestion was skipped due to low confidence.

🧰 Tools
🪛 GitHub Check: Node.js / Test (ubuntu-latest, 23)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 22)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 23)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 20)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 18)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 16)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (ubuntu-latest, 14)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 22)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 20)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 16)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 14)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 GitHub Check: Node.js / Test (macos-latest, 18)

[warning] 235-235:
it.only not permitted


[warning] 235-235:
it.only not permitted

🪛 Biome

[error] 235-235: Don't focus the test.

The 'only' method is often used for debugging or during implementation. It should be removed before deploying to production.
Consider removing 'only' to ensure all tests are executed.
Unsafe fix: Remove focus from test.

(lint/suspicious/noFocusedTests)


it('should use hostname in config', async () => {
const url = address.ip() + ':17010';

Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/apps/app-listen-reusePort/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

module.exports = app => {
// don't use the port that egg-mock defined
app._options.port = undefined;
};
9 changes: 9 additions & 0 deletions test/fixtures/apps/app-listen-reusePort/app/router.js
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;
});
};
11 changes: 11 additions & 0 deletions test/fixtures/apps/app-listen-reusePort/config/config.default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

Comment on lines +1 to +2
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove redundant 'use strict' directive.

The 'use strict' directive is unnecessary in ES modules as they are automatically in strict mode.

Apply this diff:

-'use strict';
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'use strict';
🧰 Tools
🪛 Biome

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

module.exports = {
keys: '123',
cluster: {
listen: {
port: 17010,
reusePort: true,
},
},
};
3 changes: 3 additions & 0 deletions test/fixtures/apps/app-listen-reusePort/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "app-listen-reusePort"
}
11 changes: 11 additions & 0 deletions test/master.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
70 changes: 70 additions & 0 deletions test/reuseport_cluster.js
Original file line number Diff line number Diff line change
@@ -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}`);
});
}
Comment on lines +6 to +36
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance request helper function reliability.

While the function handles basic error cases, there are several improvements that could make the tests more reliable:

Consider these enhancements:

 function request(index) {
-  http.get('http://localhost:17001/', res => {
+  const req = 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();
+      process.exitCode = 1;
       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);
+        process.exitCode = 1;
       }
     });
   }).on('error', e => {
     console.error(`Got error: ${e.stack}`);
+    process.exitCode = 1;
   });
+  
+  // Add timeout to prevent hanging tests
+  req.setTimeout(5000, () => {
+    console.error(`Request ${index} timed out`);
+    req.destroy();
+    process.exitCode = 1;
+  });
 }

Changes:

  1. Added request timeout to prevent hanging tests
  2. Set non-zero exit code on errors to fail the test
  3. Stored request object for proper cleanup
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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}`);
});
}
function request(index) {
const req = 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();
process.exitCode = 1;
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);
process.exitCode = 1;
}
});
}).on('error', e => {
console.error(`Got error: ${e.stack}`);
process.exitCode = 1;
});
// Add timeout to prevent hanging tests
req.setTimeout(5000, () => {
console.error(`Request ${index} timed out`);
req.destroy();
process.exitCode = 1;
});
}


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 {
Comment on lines +38 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve primary process test reliability and verification.

The current implementation has potential race conditions and lacks proper verification of the reusePort functionality.

Consider these improvements:

 if (cluster.isPrimary) {
   console.log(`Primary ${process.pid} is running`);
+  const workers = new Set();
 
   // Fork workers.
   for (let i = 0; i < numCPUs; i++) {
-    cluster.fork();
+    const worker = cluster.fork();
+    workers.add(worker);
   }
 
+  // Track worker readiness
+  let readyWorkers = 0;
+  cluster.on('message', (worker, message) => {
+    if (message === 'ready') {
+      readyWorkers++;
+      if (readyWorkers === numCPUs) {
+        runTests();
+      }
+    }
+  });
+
   cluster.on('exit', (worker, code, signal) => {
     console.log(`worker ${worker.process.pid} died, code: ${code}, signal: ${signal}`);
+    workers.delete(worker);
+    if (workers.size === 0) {
+      process.exit(process.exitCode || 0);
+    }
   });
 
-  setTimeout(() => {
+  function runTests() {
+    const results = new Map();
     for (let i = 0; i < 20; i++) {
-      request(i);
+      request(i, (workerId) => {
+        results.set(i, workerId);
+        if (results.size === 20) {
+          verifyDistribution(results);
+        }
+      });
     }
-  }, 2000);
-  setTimeout(() => {
-    process.exit(0);
-  }, 5000);
+  }
+
+  function verifyDistribution(results) {
+    const workerHits = new Map();
+    for (const workerId of results.values()) {
+      workerHits.set(workerId, (workerHits.get(workerId) || 0) + 1);
+    }
+    // Verify that requests were distributed across workers
+    if (workerHits.size < numCPUs) {
+      console.error('Requests were not distributed across all workers');
+      process.exitCode = 1;
+    }
+    process.exit(process.exitCode || 0);
+  }

Changes:

  1. Added worker readiness tracking
  2. Implemented request distribution verification
  3. Improved process exit handling

Committable suggestion was skipped due to low confidence.

// 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`);
}
Comment on lines +58 to +70
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance worker process reliability and error handling.

The worker process needs better error handling and lifecycle management.

Apply these improvements:

 } else {
   // Workers can share any TCP connection
   // In this case it is an HTTP server
-  http.createServer((req, res) => {
+  const server = http.createServer((req, res) => {
     res.writeHead(200);
+    res.write(`worker ${process.pid}\n`);
     res.end('hello world\n');
   }).listen({
     port: 17001,
     reusePort: true,
+  }, () => {
+    console.log(`Worker ${process.pid} started`);
+    // Notify primary that we're ready
+    process.send('ready');
   });
 
-  console.log(`Worker ${process.pid} started`);
+  // Handle errors
+  server.on('error', (err) => {
+    console.error(`Worker ${process.pid} server error:`, err);
+    process.exit(1);
+  });
+
+  // Graceful shutdown
+  process.on('SIGTERM', () => {
+    server.close(() => {
+      process.exit(0);
+    });
+  });
 }

Changes:

  1. Added server startup error handling
  2. Included worker ID in response for distribution verification
  3. Added graceful shutdown handling
  4. Implemented worker readiness notification
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} 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`);
}
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
const server = http.createServer((req, res) => {
res.writeHead(200);
res.write(`worker ${process.pid}\n`);
res.end('hello world\n');
}).listen({
port: 17001,
reusePort: true,
}, () => {
console.log(`Worker ${process.pid} started`);
// Notify primary that we're ready
process.send('ready');
});
// Handle errors
server.on('error', (err) => {
console.error(`Worker ${process.pid} server error:`, err);
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', () => {
server.close(() => {
process.exit(0);
});
});
}

Loading