@eggjs/cluster 4.0.0-beta.36 → 4.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { AppProcessWorker } from "./utils/mode/impl/process/app.js";
2
2
  import { AppThreadWorker } from "./utils/mode/impl/worker_threads/app.js";
3
3
  import fs from "node:fs";
4
+ import os from "node:os";
4
5
  import { debuglog } from "node:util";
5
6
  import { EggConsoleLogger } from "egg-logger";
6
7
  import { importModule } from "@eggjs/utils";
@@ -9,6 +10,12 @@ import { createServer as createServer$1 } from "node:https";
9
10
 
10
11
  //#region src/app_worker.ts
11
12
  const debug = debuglog("egg/cluster/app_worker");
13
+ const REUSE_PORT_SUPPORTED_PLATFORMS = [
14
+ "linux",
15
+ "freebsd",
16
+ "sunos",
17
+ "aix"
18
+ ];
12
19
  async function main() {
13
20
  const options = JSON.parse(process.argv[2]);
14
21
  if (options.require) for (const mod of options.require) await importModule(mod, { paths: [options.baseDir] });
@@ -50,7 +57,12 @@ async function main() {
50
57
  const port = app.options.port = options.port || listenConfig.port;
51
58
  const debugPort = options.debugPort;
52
59
  const protocol = httpsOptions.key && httpsOptions.cert ? "https" : "http";
53
- debug("[app_worker:%s] listenConfig: %j, real port: %o, protocol: %o, debugPort: %o", process.pid, listenConfig, port, protocol, debugPort);
60
+ let reusePort = options.reusePort ?? listenConfig.reusePort ?? false;
61
+ if (reusePort && !REUSE_PORT_SUPPORTED_PLATFORMS.includes(os.platform())) {
62
+ reusePort = false;
63
+ debug("[app_worker:%s] platform %s is not supported for reusePort, set reusePort to false", process.pid, os.platform());
64
+ }
65
+ debug("[app_worker:%s] listenConfig: %j, real port: %o, protocol: %o, debugPort: %o, reusePort: %o", process.pid, listenConfig, port, protocol, debugPort, reusePort);
54
66
  AppWorker.send({
55
67
  to: "master",
56
68
  action: "realport",
@@ -92,10 +104,20 @@ async function main() {
92
104
  exitProcess();
93
105
  return;
94
106
  }
95
- const args = [port];
96
- if (listenConfig.hostname) args.push(listenConfig.hostname);
97
- debug("listen options %j", args);
98
- server.listen(...args);
107
+ if (reusePort) {
108
+ const listenOptions = {
109
+ port,
110
+ reusePort
111
+ };
112
+ if (listenConfig.hostname) listenOptions.host = listenConfig.hostname;
113
+ debug("[app_worker:%s] listen with reusePort options %j", process.pid, listenOptions);
114
+ server.listen(listenOptions);
115
+ } else {
116
+ const args = [port];
117
+ if (listenConfig.hostname) args.push(listenConfig.hostname);
118
+ debug("listen options %j", args);
119
+ server.listen(...args);
120
+ }
99
121
  }
100
122
  if (debugPortServer) {
101
123
  debug("listen on debug port: %s", debugPort);
@@ -108,14 +130,15 @@ async function main() {
108
130
  address,
109
131
  addressType: -1
110
132
  };
111
- debug("[app_worker:%s] listening at %j", process.pid, address);
133
+ debug("[app_worker:%s] listening at %j, reusePort: %o", process.pid, address, reusePort);
112
134
  AppWorker.send({
113
135
  to: "master",
114
136
  action: "app-start",
115
137
  data: {
116
138
  address,
117
139
  workerId: AppWorker.workerId
118
- }
140
+ },
141
+ reusePort
119
142
  });
120
143
  });
121
144
  }
@@ -14,6 +14,12 @@ interface MessageBody {
14
14
  receiverPid?: string;
15
15
  receiverWorkerId?: string;
16
16
  senderWorkerId?: string;
17
+ /**
18
+ * Whether reusePort is enabled for server listen.
19
+ * When reusePort is true, cluster won't get `listening` event,
20
+ * so we need to use cluster `message` event instead.
21
+ */
22
+ reusePort?: boolean;
17
23
  }
18
24
  /**
19
25
  * master messenger, provide communication between parent, master, agent and app.
@@ -1,11 +1,13 @@
1
1
  import { terminate } from "../../../terminate.js";
2
2
  import { BaseAppUtils, BaseAppWorker } from "../../base/app.js";
3
+ import { debuglog } from "node:util";
3
4
  import { graceful } from "graceful-process";
4
5
  import { sendmessage } from "sendmessage";
5
6
  import cluster from "node:cluster";
6
7
  import { cfork } from "cfork";
7
8
 
8
9
  //#region src/utils/mode/impl/process/app.ts
10
+ const debug = debuglog("egg/cluster/utils/mode/impl/process/app");
9
11
  var AppProcessWorker = class extends BaseAppWorker {
10
12
  get id() {
11
13
  return this.instance.id;
@@ -33,6 +35,11 @@ var AppProcessWorker = class extends BaseAppWorker {
33
35
  }
34
36
  static send(message) {
35
37
  message.senderWorkerId = String(process.pid);
38
+ if (message.action === "app-start" && message.reusePort) {
39
+ debug("send app-start message with reusePort, use cluster.worker.send()");
40
+ cluster.worker.send(message);
41
+ return;
42
+ }
36
43
  process.send(message);
37
44
  }
38
45
  static kill() {
@@ -103,15 +103,23 @@ var AppThreadUtils = class extends BaseAppUtils {
103
103
  fork() {
104
104
  this.startTime = Date.now();
105
105
  this.startSuccessCount = 0;
106
- const ports = this.options.ports ?? [];
107
- if (!ports.length) ports.push(this.options.port);
108
- this.options.workers = ports.length;
109
- let i = 0;
110
- do {
111
- const options = Object.assign({}, this.options, { port: ports[i] });
112
- const argv = [JSON.stringify(options)];
113
- this.#forkSingle(this.getAppWorkerFile(), { argv }, ++i);
114
- } while (i < ports.length);
106
+ if (this.options.reusePort) {
107
+ if (!this.options.port) throw new Error("options.port must be specified when reusePort is enabled");
108
+ for (let i = 0; i < this.options.workers; i++) {
109
+ const argv = [JSON.stringify(this.options)];
110
+ this.#forkSingle(this.getAppWorkerFile(), { argv }, i + 1);
111
+ }
112
+ } else {
113
+ const ports = this.options.ports ?? [];
114
+ if (!ports.length) ports.push(this.options.port);
115
+ this.options.workers = ports.length;
116
+ let i = 0;
117
+ do {
118
+ const options = Object.assign({}, this.options, { port: ports[i] });
119
+ const argv = [JSON.stringify(options)];
120
+ this.#forkSingle(this.getAppWorkerFile(), { argv }, ++i);
121
+ } while (i < ports.length);
122
+ }
115
123
  return this;
116
124
  }
117
125
  async kill() {
@@ -68,6 +68,12 @@ interface ClusterOptions {
68
68
  * sticky mode server
69
69
  */
70
70
  sticky?: boolean;
71
+ /**
72
+ * enable SO_REUSEPORT socket option for server listen, default is `false`.
73
+ * Only available on Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+, Solaris 11.4, and AIX 7.2.5+.
74
+ * @see https://nodejs.org/api/net.html#serverlistenoptions-callback
75
+ */
76
+ reusePort?: boolean;
71
77
  /** customized plugins, for unittest */
72
78
  plugins?: object;
73
79
  isDebug?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eggjs/cluster",
3
- "version": "4.0.0-beta.36",
3
+ "version": "4.0.1-beta.0",
4
4
  "description": "cluster manager for egg",
5
5
  "keywords": [
6
6
  "cluster",
@@ -45,7 +45,7 @@
45
45
  "sendmessage": "^3.0.1",
46
46
  "terminal-link": "^5.0.0",
47
47
  "utility": "^2.5.0",
48
- "@eggjs/utils": "5.0.0-beta.36"
48
+ "@eggjs/utils": "5.0.1-beta.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "address": "2",
@@ -54,10 +54,10 @@
54
54
  "typescript": "^5.9.3",
55
55
  "urllib": "^4.8.2",
56
56
  "vitest": "^4.0.15",
57
- "@eggjs/mock": "7.0.0-beta.36",
58
- "@eggjs/errors": "3.0.0-beta.36",
59
- "@eggjs/supertest": "9.0.0-beta.36",
60
- "@eggjs/tsconfig": "3.1.0-beta.36"
57
+ "@eggjs/errors": "3.0.1-beta.0",
58
+ "@eggjs/mock": "7.0.1-beta.0",
59
+ "@eggjs/supertest": "9.0.1-beta.0",
60
+ "@eggjs/tsconfig": "3.1.1-beta.0"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=22.18.0"