@eggjs/cluster 4.0.2-beta.24 → 4.0.2-beta.26

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,5 +1,6 @@
1
- import { AgentProcessWorker } from "./utils/mode/impl/process/agent.js";
2
- import { AgentThreadWorker } from "./utils/mode/impl/worker_threads/agent.js";
1
+ import { startAgentWorker } from "./worker_protocol/agent.js";
2
+ import { createProcessWorkerIO } from "./worker_protocol/process.js";
3
+ import { createWorkerThreadIO } from "./worker_protocol/worker-thread.js";
3
4
  import { debuglog } from "node:util";
4
5
  import { EggConsoleLogger } from "egg-logger";
5
6
  import { importModule } from "@eggjs/utils";
@@ -16,9 +17,7 @@ const debug = debuglog("egg/cluster/agent_worker");
16
17
  async function main() {
17
18
  const options = JSON.parse(process.argv[2]);
18
19
  if (options.require) for (const mod of options.require) await importModule(mod, { paths: [options.baseDir] });
19
- let AgentWorker;
20
- if (options.startMode === "worker_threads") AgentWorker = AgentThreadWorker;
21
- else AgentWorker = AgentProcessWorker;
20
+ const workerIO = options.startMode === "worker_threads" ? createWorkerThreadIO() : createProcessWorkerIO();
22
21
  const consoleLogger = new EggConsoleLogger({ level: process.env.EGG_AGENT_WORKER_LOGGER_LEVEL });
23
22
  const { Agent } = await importModule(options.framework, { paths: [options.baseDir] });
24
23
  debug("new Agent with options %j", options);
@@ -29,25 +28,7 @@ async function main() {
29
28
  consoleLogger.error(err);
30
29
  throw err;
31
30
  }
32
- function startErrorHandler(err) {
33
- consoleLogger.error(err);
34
- consoleLogger.error("[agent_worker] start error, exiting with code:1");
35
- AgentWorker.kill();
36
- }
37
- agent.ready((err) => {
38
- if (err) return;
39
- agent.removeListener("error", startErrorHandler);
40
- AgentWorker.send({
41
- action: "agent-start",
42
- to: "master"
43
- });
44
- });
45
- agent.once("error", startErrorHandler);
46
- AgentWorker.gracefulExit({
47
- logger: consoleLogger,
48
- label: "agent_worker",
49
- beforeExit: () => agent.close()
50
- });
31
+ startAgentWorker(agent, workerIO, consoleLogger);
51
32
  }
52
33
  main();
53
34
 
@@ -1,27 +1,16 @@
1
- import { AppProcessWorker } from "./utils/mode/impl/process/app.js";
2
- import { AppThreadWorker } from "./utils/mode/impl/worker_threads/app.js";
3
- import fs from "node:fs";
4
- import os from "node:os";
1
+ import { createProcessWorkerIO } from "./worker_protocol/process.js";
2
+ import { createWorkerThreadIO } from "./worker_protocol/worker-thread.js";
3
+ import { startAppWorker } from "./worker_protocol/app.js";
5
4
  import { debuglog } from "node:util";
6
5
  import { EggConsoleLogger } from "egg-logger";
7
6
  import { importModule } from "@eggjs/utils";
8
- import { createServer } from "node:http";
9
- import { createServer as createServer$1 } from "node:https";
10
7
 
11
8
  //#region src/app_worker.ts
12
9
  const debug = debuglog("egg/cluster/app_worker");
13
- const REUSE_PORT_SUPPORTED_PLATFORMS = [
14
- "linux",
15
- "freebsd",
16
- "sunos",
17
- "aix"
18
- ];
19
10
  async function main() {
20
11
  const options = JSON.parse(process.argv[2]);
21
12
  if (options.require) for (const mod of options.require) await importModule(mod, { paths: [options.baseDir] });
22
- let AppWorker;
23
- if (options.startMode === "worker_threads") AppWorker = AppThreadWorker;
24
- else AppWorker = AppProcessWorker;
13
+ const workerIO = options.startMode === "worker_threads" ? createWorkerThreadIO() : createProcessWorkerIO();
25
14
  const consoleLogger = new EggConsoleLogger({ level: process.env.EGG_APP_WORKER_LOGGER_LEVEL });
26
15
  const { Application } = await importModule(options.framework, { paths: [options.baseDir] });
27
16
  debug("[app_worker:%s] new Application with options %j", process.pid, options);
@@ -32,121 +21,7 @@ async function main() {
32
21
  consoleLogger.error(err);
33
22
  throw err;
34
23
  }
35
- app.ready(startServer);
36
- function exitProcess() {
37
- AppWorker.kill();
38
- }
39
- app.once("startTimeout", startTimeoutHandler);
40
- function startTimeoutHandler() {
41
- consoleLogger.error("[app_worker] start timeout, exiting with code:1");
42
- exitProcess();
43
- }
44
- function startServer(err) {
45
- if (err) {
46
- consoleLogger.error(err);
47
- consoleLogger.error("[app_worker] start error, exiting with code:1");
48
- exitProcess();
49
- return;
50
- }
51
- const clusterConfig = app.config.cluster ?? {};
52
- const listenConfig = clusterConfig.listen ?? {};
53
- const httpsOptions = {
54
- ...clusterConfig.https,
55
- ...options.https
56
- };
57
- const port = app.options.port = options.port || listenConfig.port;
58
- const debugPort = options.debugPort;
59
- const protocol = httpsOptions.key && httpsOptions.cert ? "https" : "http";
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);
66
- AppWorker.send({
67
- to: "master",
68
- action: "realport",
69
- data: {
70
- port,
71
- protocol
72
- }
73
- });
74
- app.removeListener("startTimeout", startTimeoutHandler);
75
- let server;
76
- let debugPortServer;
77
- if (protocol === "https") {
78
- httpsOptions.key = fs.readFileSync(httpsOptions.key);
79
- httpsOptions.cert = fs.readFileSync(httpsOptions.cert);
80
- httpsOptions.ca = httpsOptions.ca && fs.readFileSync(httpsOptions.ca);
81
- server = createServer$1(httpsOptions, app.callback());
82
- if (debugPort) debugPortServer = createServer(app.callback());
83
- } else {
84
- server = createServer(app.callback());
85
- if (debugPort) debugPortServer = server;
86
- }
87
- server.once("error", (err$1) => {
88
- consoleLogger.error("[app_worker] server got error: %s, code: %s", err$1.message, err$1.code);
89
- exitProcess();
90
- });
91
- app.emit("server", server);
92
- if (options.sticky && options.stickyWorkerPort) {
93
- server.listen(options.stickyWorkerPort, "127.0.0.1");
94
- AppWorker.on("message", (message, connection) => {
95
- if (message !== "sticky-session:connection") return;
96
- server.emit("connection", connection);
97
- connection.resume();
98
- });
99
- } else {
100
- if (listenConfig.path) server.listen(listenConfig.path);
101
- else {
102
- if (typeof port !== "number") {
103
- consoleLogger.error("[app_worker:%s] port should be number, but got %s(%s)", process.pid, port, typeof port);
104
- exitProcess();
105
- return;
106
- }
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
- }
121
- }
122
- if (debugPortServer) {
123
- debug("listen on debug port: %s", debugPort);
124
- debugPortServer.listen(debugPort);
125
- }
126
- }
127
- server.once("listening", () => {
128
- let address = server.address() || { port };
129
- if (typeof address === "string") address = {
130
- address,
131
- addressType: -1
132
- };
133
- debug("[app_worker:%s] listening at %j, reusePort: %o", process.pid, address, reusePort);
134
- AppWorker.send({
135
- to: "master",
136
- action: "app-start",
137
- data: {
138
- address,
139
- workerId: AppWorker.workerId
140
- },
141
- reusePort
142
- });
143
- });
144
- }
145
- AppWorker.gracefulExit({
146
- logger: consoleLogger,
147
- label: "app_worker",
148
- beforeExit: () => app.close()
149
- });
24
+ startAppWorker(app, options, workerIO, consoleLogger);
150
25
  }
151
26
  main();
152
27
 
@@ -1,15 +1,11 @@
1
1
  import { BaseAgentUtils, BaseAgentWorker } from "../../base/agent.js";
2
2
  import { MessageBody } from "../../../messenger.js";
3
3
  import { ChildProcess } from "node:child_process";
4
- import { Options } from "graceful-process";
5
4
 
6
5
  //#region src/utils/mode/impl/process/agent.d.ts
7
6
  declare class AgentProcessWorker extends BaseAgentWorker<ChildProcess> {
8
7
  get workerId(): number;
9
8
  send(message: MessageBody): void;
10
- static send(message: MessageBody): void;
11
- static kill(): void;
12
- static gracefulExit(options: Options): void;
13
9
  }
14
10
  declare class AgentProcessUtils extends BaseAgentUtils {
15
11
  #private;
@@ -3,7 +3,6 @@ import { terminate } from "../../../terminate.js";
3
3
  import { BaseAgentUtils, BaseAgentWorker } from "../../base/agent.js";
4
4
  import { debuglog } from "node:util";
5
5
  import { fork } from "node:child_process";
6
- import { graceful } from "graceful-process";
7
6
  import { sendmessage } from "sendmessage";
8
7
 
9
8
  //#region src/utils/mode/impl/process/agent.ts
@@ -15,17 +14,6 @@ var AgentProcessWorker = class extends BaseAgentWorker {
15
14
  send(message) {
16
15
  sendmessage(this.instance, message);
17
16
  }
18
- static send(message) {
19
- message.senderWorkerId = String(process.pid);
20
- process.send(message);
21
- }
22
- static kill() {
23
- process.exitCode = 1;
24
- process.kill(process.pid);
25
- }
26
- static gracefulExit(options) {
27
- graceful(options);
28
- }
29
17
  };
30
18
  var AgentProcessUtils = class extends BaseAgentUtils {
31
19
  #agentProcess;
@@ -38,8 +26,13 @@ var AgentProcessUtils = class extends BaseAgentUtils {
38
26
  if (process.platform === "win32") forkOptions.windowsHide = true;
39
27
  const debugPort = process.env.EGG_AGENT_DEBUG_PORT ?? 5800;
40
28
  if (this.options.isDebug) forkOptions.execArgv = process.execArgv.concat([`--inspect-port=${debugPort}`]);
29
+ if (this.options.agentSnapshotBlob) forkOptions.execArgv = [
30
+ ...forkOptions.execArgv ?? process.execArgv,
31
+ "--snapshot-blob",
32
+ this.options.agentSnapshotBlob
33
+ ];
41
34
  debug("forkOptions: %j, args: %s", forkOptions, args);
42
- const agentProcess = this.#agentProcess = fork(this.getAgentWorkerFile(), args, forkOptions);
35
+ const agentProcess = this.#agentProcess = fork(this.options.agentWorkerFile || this.getAgentWorkerFile(), args, forkOptions);
43
36
  const agentWorker = this.instance = new AgentProcessWorker(agentProcess);
44
37
  agentWorker.status = "starting";
45
38
  agentWorker.id = ++this.#id;
@@ -93,4 +86,4 @@ var AgentProcessUtils = class extends BaseAgentUtils {
93
86
  };
94
87
 
95
88
  //#endregion
96
- export { AgentProcessUtils, AgentProcessWorker };
89
+ export { AgentProcessUtils };
@@ -1,6 +1,5 @@
1
1
  import { BaseAppUtils, BaseAppWorker } from "../../base/app.js";
2
2
  import { MessageBody } from "../../../messenger.js";
3
- import { Options } from "graceful-process";
4
3
  import { Worker } from "node:cluster";
5
4
 
6
5
  //#region src/utils/mode/impl/process/app.d.ts
@@ -11,11 +10,6 @@ declare class AppProcessWorker extends BaseAppWorker<Worker> {
11
10
  get exitCode(): number;
12
11
  send(message: MessageBody): void;
13
12
  clean(): void;
14
- static get workerId(): number;
15
- static on(event: string, listener: (...args: any[]) => void): void;
16
- static send(message: MessageBody): void;
17
- static kill(): void;
18
- static gracefulExit(options: Options): void;
19
13
  }
20
14
  declare class AppProcessUtils extends BaseAppUtils {
21
15
  fork(): this;
@@ -1,13 +1,10 @@
1
1
  import { terminate } from "../../../terminate.js";
2
2
  import { BaseAppUtils, BaseAppWorker } from "../../base/app.js";
3
- import { debuglog } from "node:util";
4
- import { graceful } from "graceful-process";
5
3
  import { sendmessage } from "sendmessage";
6
4
  import cluster from "node:cluster";
7
5
  import { cfork } from "cfork";
8
6
 
9
7
  //#region src/utils/mode/impl/process/app.ts
10
- const debug = debuglog("egg/cluster/utils/mode/impl/process/app");
11
8
  var AppProcessWorker = class extends BaseAppWorker {
12
9
  get id() {
13
10
  return this.instance.id;
@@ -27,28 +24,6 @@ var AppProcessWorker = class extends BaseAppWorker {
27
24
  clean() {
28
25
  this.instance.removeAllListeners();
29
26
  }
30
- static get workerId() {
31
- return process.pid;
32
- }
33
- static on(event, listener) {
34
- process.on(event, listener);
35
- }
36
- static send(message) {
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
- }
43
- process.send(message);
44
- }
45
- static kill() {
46
- process.exitCode = 1;
47
- process.kill(process.pid);
48
- }
49
- static gracefulExit(options) {
50
- graceful(options);
51
- }
52
27
  };
53
28
  var AppProcessUtils = class extends BaseAppUtils {
54
29
  fork() {
@@ -56,8 +31,14 @@ var AppProcessUtils = class extends BaseAppUtils {
56
31
  this.startSuccessCount = 0;
57
32
  const args = [JSON.stringify(this.options)];
58
33
  this.log("[master] start appWorker with args %j (process)", args);
34
+ const execArgv = this.options.appSnapshotBlob ? [
35
+ ...process.execArgv,
36
+ "--snapshot-blob",
37
+ this.options.appSnapshotBlob
38
+ ] : void 0;
59
39
  cfork({
60
- exec: this.getAppWorkerFile(),
40
+ exec: this.options.appWorkerFile || this.getAppWorkerFile(),
41
+ ...execArgv ? { execArgv } : {},
61
42
  args,
62
43
  silent: false,
63
44
  count: this.options.workers,
@@ -121,4 +102,4 @@ var AppProcessUtils = class extends BaseAppUtils {
121
102
  };
122
103
 
123
104
  //#endregion
124
- export { AppProcessUtils, AppProcessWorker };
105
+ export { AppProcessUtils };
@@ -1,15 +1,11 @@
1
1
  import { BaseAgentUtils, BaseAgentWorker } from "../../base/agent.js";
2
2
  import { MessageBody } from "../../../messenger.js";
3
3
  import { Worker } from "node:worker_threads";
4
- import { Options } from "graceful-process";
5
4
 
6
5
  //#region src/utils/mode/impl/worker_threads/agent.d.ts
7
6
  declare class AgentThreadWorker extends BaseAgentWorker<Worker> {
8
7
  get workerId(): number;
9
8
  send(message: MessageBody): void;
10
- static send(message: MessageBody): void;
11
- static kill(): void;
12
- static gracefulExit(options: Options): void;
13
9
  }
14
10
  declare class AgentThreadUtils extends BaseAgentUtils {
15
11
  #private;
@@ -1,7 +1,6 @@
1
1
  import { ClusterAgentWorkerError } from "../../../../error/ClusterAgentWorkerError.js";
2
2
  import { BaseAgentUtils, BaseAgentWorker } from "../../base/agent.js";
3
3
  import workerThreads from "node:worker_threads";
4
- import "graceful-process";
5
4
 
6
5
  //#region src/utils/mode/impl/worker_threads/agent.ts
7
6
  var AgentThreadWorker = class extends BaseAgentWorker {
@@ -11,20 +10,6 @@ var AgentThreadWorker = class extends BaseAgentWorker {
11
10
  send(message) {
12
11
  this.instance.postMessage(message);
13
12
  }
14
- static send(message) {
15
- message.senderWorkerId = String(workerThreads.threadId);
16
- workerThreads.parentPort.postMessage(message);
17
- }
18
- static kill() {
19
- process.exit(1);
20
- }
21
- static gracefulExit(options) {
22
- const { beforeExit } = options;
23
- process.on("exit", async (code) => {
24
- if (typeof beforeExit === "function") await beforeExit();
25
- process.exit(code);
26
- });
27
- }
28
13
  };
29
14
  var AgentThreadUtils = class extends BaseAgentUtils {
30
15
  #worker;
@@ -33,7 +18,7 @@ var AgentThreadUtils = class extends BaseAgentUtils {
33
18
  fork() {
34
19
  this.startTime = Date.now();
35
20
  const argv = [JSON.stringify(this.options)];
36
- const agentPath = this.getAgentWorkerFile();
21
+ const agentPath = this.options.agentWorkerFile || this.getAgentWorkerFile();
37
22
  const worker = this.#worker = new workerThreads.Worker(agentPath, { argv });
38
23
  const agentWorker = this.instance = new AgentThreadWorker(worker);
39
24
  this.emit("agent_forked", agentWorker);
@@ -76,4 +61,4 @@ var AgentThreadUtils = class extends BaseAgentUtils {
76
61
  };
77
62
 
78
63
  //#endregion
79
- export { AgentThreadUtils, AgentThreadWorker };
64
+ export { AgentThreadUtils };
@@ -1,7 +1,6 @@
1
1
  import { BaseAppUtils, BaseAppWorker } from "../../base/app.js";
2
2
  import { MessageBody } from "../../../messenger.js";
3
3
  import { Worker } from "node:worker_threads";
4
- import { Options } from "graceful-process";
5
4
 
6
5
  //#region src/utils/mode/impl/worker_threads/app.d.ts
7
6
  declare class AppThreadWorker extends BaseAppWorker<Worker> {
@@ -15,11 +14,6 @@ declare class AppThreadWorker extends BaseAppWorker<Worker> {
15
14
  get exitCode(): number;
16
15
  send(message: MessageBody): void;
17
16
  clean(): void;
18
- static get workerId(): number;
19
- static on(event: string, listener: (...args: any[]) => void): void;
20
- static send(message: MessageBody): void;
21
- static kill(): void;
22
- static gracefulExit(options: Options): void;
23
17
  }
24
18
  declare class AppThreadUtils extends BaseAppUtils {
25
19
  #private;
@@ -1,5 +1,5 @@
1
1
  import { BaseAppUtils, BaseAppWorker } from "../../base/app.js";
2
- import { Worker, parentPort, threadId } from "node:worker_threads";
2
+ import { Worker } from "node:worker_threads";
3
3
  import { setTimeout } from "node:timers/promises";
4
4
 
5
5
  //#region src/utils/mode/impl/worker_threads/app.ts
@@ -34,25 +34,6 @@ var AppThreadWorker = class extends BaseAppWorker {
34
34
  clean() {
35
35
  this.instance.removeAllListeners();
36
36
  }
37
- static get workerId() {
38
- return threadId;
39
- }
40
- static on(event, listener) {
41
- parentPort.on(event, listener);
42
- }
43
- static send(message) {
44
- message.senderWorkerId = String(threadId);
45
- parentPort.postMessage(message);
46
- }
47
- static kill() {
48
- process.exit(1);
49
- }
50
- static gracefulExit(options) {
51
- process.on("exit", async (code) => {
52
- if (typeof options.beforeExit === "function") await options.beforeExit();
53
- process.exit(code);
54
- });
55
- }
56
37
  };
57
38
  var AppThreadUtils = class extends BaseAppUtils {
58
39
  #workers = [];
@@ -103,11 +84,12 @@ var AppThreadUtils = class extends BaseAppUtils {
103
84
  fork() {
104
85
  this.startTime = Date.now();
105
86
  this.startSuccessCount = 0;
87
+ const appWorkerFile = this.options.appWorkerFile || this.getAppWorkerFile();
106
88
  if (this.options.reusePort) {
107
89
  if (!this.options.port) throw new Error("options.port must be specified when reusePort is enabled");
108
90
  for (let i = 0; i < this.options.workers; i++) {
109
91
  const argv = [JSON.stringify(this.options)];
110
- this.#forkSingle(this.getAppWorkerFile(), { argv }, i + 1);
92
+ this.#forkSingle(appWorkerFile, { argv }, i + 1);
111
93
  }
112
94
  } else {
113
95
  const ports = this.options.ports ?? [];
@@ -117,7 +99,7 @@ var AppThreadUtils = class extends BaseAppUtils {
117
99
  do {
118
100
  const options = Object.assign({}, this.options, { port: ports[i] });
119
101
  const argv = [JSON.stringify(options)];
120
- this.#forkSingle(this.getAppWorkerFile(), { argv }, ++i);
102
+ this.#forkSingle(appWorkerFile, { argv }, ++i);
121
103
  } while (i < ports.length);
122
104
  }
123
105
  return this;
@@ -133,4 +115,4 @@ var AppThreadUtils = class extends BaseAppUtils {
133
115
  };
134
116
 
135
117
  //#endregion
136
- export { AppThreadUtils, AppThreadWorker };
118
+ export { AppThreadUtils };
@@ -61,6 +61,22 @@ interface ClusterOptions {
61
61
  */
62
62
  startMode?: ClusterStartMode;
63
63
  /**
64
+ * custom app worker file
65
+ */
66
+ appWorkerFile?: string;
67
+ /**
68
+ * custom agent worker file
69
+ */
70
+ agentWorkerFile?: string;
71
+ /**
72
+ * V8 startup snapshot blob used to restore app workers, only available in process mode
73
+ */
74
+ appSnapshotBlob?: string;
75
+ /**
76
+ * V8 startup snapshot blob used to restore the agent worker, only available in process mode
77
+ */
78
+ agentSnapshotBlob?: string;
79
+ /**
64
80
  * startup port of each app worker, such as: `[7001, 7002, 7003]`, only effects when the startMode is `'worker_threads'`
65
81
  */
66
82
  ports?: number[];
@@ -46,6 +46,25 @@ async function parseOptions(options) {
46
46
  if (options.require) {
47
47
  if (typeof options.require === "string") options.require = [options.require];
48
48
  }
49
+ for (const optionName of ["appWorkerFile", "agentWorkerFile"]) {
50
+ const workerFile = options[optionName];
51
+ if (!workerFile) {
52
+ options[optionName] = void 0;
53
+ continue;
54
+ }
55
+ options[optionName] = path.resolve(options.baseDir, workerFile);
56
+ assert(fs.existsSync(options[optionName]), `options.${optionName} file should exist: ${options[optionName]}`);
57
+ }
58
+ for (const optionName of ["appSnapshotBlob", "agentSnapshotBlob"]) {
59
+ const snapshotBlob = options[optionName];
60
+ if (!snapshotBlob) {
61
+ options[optionName] = void 0;
62
+ continue;
63
+ }
64
+ options[optionName] = path.resolve(options.baseDir, snapshotBlob);
65
+ assert(fs.existsSync(options[optionName]), `options.${optionName} file should exist: ${options[optionName]}`);
66
+ assert(options.startMode !== "worker_threads", `options.${optionName} only supports startMode "process"`);
67
+ }
49
68
  if (process.env.NODE_ENV === "production") process.env.NO_DEPRECATION = "*";
50
69
  const isDebug = process.execArgv.some((argv) => argv.includes("--debug") || argv.includes("--inspect"));
51
70
  if (isDebug) options.isDebug = isDebug;
@@ -0,0 +1,23 @@
1
+ import { MessageBody } from "../utils/messenger.js";
2
+ import { EggConsoleLogger } from "egg-logger";
3
+ import { Options } from "graceful-process";
4
+
5
+ //#region src/worker_protocol/agent.d.ts
6
+
7
+ /**
8
+ * Transport used by the agent-worker protocol. Process and worker-thread
9
+ * entries provide different implementations of this interface.
10
+ */
11
+ interface AgentWorkerIO {
12
+ send(message: MessageBody): void;
13
+ kill(): void;
14
+ gracefulExit(options: Options): void;
15
+ }
16
+ /**
17
+ * Run the agent-worker side of the Egg cluster protocol on an already
18
+ * constructed agent: report `agent-start` on readiness, exit on start error,
19
+ * and wire graceful exit.
20
+ */
21
+ declare function startAgentWorker(agent: any, io: AgentWorkerIO, consoleLogger?: EggConsoleLogger): void;
22
+ //#endregion
23
+ export { AgentWorkerIO, startAgentWorker };
@@ -0,0 +1,32 @@
1
+ import { EggConsoleLogger } from "egg-logger";
2
+
3
+ //#region src/worker_protocol/agent.ts
4
+ /**
5
+ * Run the agent-worker side of the Egg cluster protocol on an already
6
+ * constructed agent: report `agent-start` on readiness, exit on start error,
7
+ * and wire graceful exit.
8
+ */
9
+ function startAgentWorker(agent, io, consoleLogger = new EggConsoleLogger({ level: process.env.EGG_AGENT_WORKER_LOGGER_LEVEL })) {
10
+ function startErrorHandler(err) {
11
+ consoleLogger.error(err);
12
+ consoleLogger.error("[agent_worker] start error, exiting with code:1");
13
+ io.kill();
14
+ }
15
+ agent.once("error", startErrorHandler);
16
+ agent.ready((err) => {
17
+ if (err) return;
18
+ agent.removeListener("error", startErrorHandler);
19
+ io.send({
20
+ action: "agent-start",
21
+ to: "master"
22
+ });
23
+ });
24
+ io.gracefulExit({
25
+ logger: consoleLogger,
26
+ label: "agent_worker",
27
+ beforeExit: () => agent.close()
28
+ });
29
+ }
30
+
31
+ //#endregion
32
+ export { startAgentWorker };
@@ -0,0 +1,30 @@
1
+ import { AgentWorkerIO } from "./agent.js";
2
+ import { EggConsoleLogger } from "egg-logger";
3
+
4
+ //#region src/worker_protocol/app.d.ts
5
+
6
+ /**
7
+ * Transport used by the app-worker protocol. It extends the agent surface
8
+ * with app-specific worker identity and sticky-session message handling.
9
+ */
10
+ interface AppWorkerIO extends AgentWorkerIO {
11
+ readonly workerId: number | string;
12
+ on(event: string, listener: (...args: any[]) => void): void;
13
+ }
14
+ interface AppWorkerProtocolOptions {
15
+ port?: number;
16
+ debugPort?: number;
17
+ https?: object;
18
+ sticky?: boolean;
19
+ stickyWorkerPort?: number;
20
+ reusePort?: boolean;
21
+ }
22
+ /**
23
+ * Run the app-worker side of the Egg cluster protocol on an already
24
+ * constructed application: wait for readiness, create the HTTP(S) server
25
+ * (sticky / reusePort / debug port variants), report `realport` and
26
+ * `app-start` to the master, and wire graceful exit.
27
+ */
28
+ declare function startAppWorker(app: any, options: AppWorkerProtocolOptions, io: AppWorkerIO, consoleLogger?: EggConsoleLogger): void;
29
+ //#endregion
30
+ export { AppWorkerIO, AppWorkerProtocolOptions, startAppWorker };
@@ -0,0 +1,141 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import { debuglog } from "node:util";
4
+ import { EggConsoleLogger } from "egg-logger";
5
+ import { createServer } from "node:http";
6
+ import { createServer as createServer$1 } from "node:https";
7
+
8
+ //#region src/worker_protocol/app.ts
9
+ const debug = debuglog("egg/cluster/worker_protocol/app");
10
+ const REUSE_PORT_SUPPORTED_PLATFORMS = [
11
+ "linux",
12
+ "freebsd",
13
+ "sunos",
14
+ "aix"
15
+ ];
16
+ /**
17
+ * Run the app-worker side of the Egg cluster protocol on an already
18
+ * constructed application: wait for readiness, create the HTTP(S) server
19
+ * (sticky / reusePort / debug port variants), report `realport` and
20
+ * `app-start` to the master, and wire graceful exit.
21
+ */
22
+ function startAppWorker(app, options, io, consoleLogger = new EggConsoleLogger({ level: process.env.EGG_APP_WORKER_LOGGER_LEVEL })) {
23
+ function exitProcess() {
24
+ io.kill();
25
+ }
26
+ function startTimeoutHandler() {
27
+ consoleLogger.error("[app_worker] start timeout, exiting with code:1");
28
+ exitProcess();
29
+ }
30
+ app.once("startTimeout", startTimeoutHandler);
31
+ app.ready(startServer);
32
+ function startServer(err) {
33
+ if (err) {
34
+ consoleLogger.error(err);
35
+ consoleLogger.error("[app_worker] start error, exiting with code:1");
36
+ exitProcess();
37
+ return;
38
+ }
39
+ const clusterConfig = app.config.cluster ?? {};
40
+ const listenConfig = clusterConfig.listen ?? {};
41
+ const httpsOptions = {
42
+ ...clusterConfig.https,
43
+ ...options.https
44
+ };
45
+ const port = app.options.port = options.port || listenConfig.port;
46
+ const debugPort = options.debugPort;
47
+ const protocol = httpsOptions.key && httpsOptions.cert ? "https" : "http";
48
+ let reusePort = options.reusePort ?? listenConfig.reusePort ?? false;
49
+ if (reusePort && !REUSE_PORT_SUPPORTED_PLATFORMS.includes(os.platform())) {
50
+ reusePort = false;
51
+ debug("[app_worker:%s] platform %s is not supported for reusePort, set reusePort to false", process.pid, os.platform());
52
+ }
53
+ debug("[app_worker:%s] listenConfig: %j, real port: %o, protocol: %o, debugPort: %o, reusePort: %o", process.pid, listenConfig, port, protocol, debugPort, reusePort);
54
+ io.send({
55
+ to: "master",
56
+ action: "realport",
57
+ data: {
58
+ port,
59
+ protocol
60
+ }
61
+ });
62
+ app.removeListener("startTimeout", startTimeoutHandler);
63
+ let server;
64
+ let debugPortServer;
65
+ if (protocol === "https") {
66
+ httpsOptions.key = fs.readFileSync(httpsOptions.key);
67
+ httpsOptions.cert = fs.readFileSync(httpsOptions.cert);
68
+ httpsOptions.ca = httpsOptions.ca && fs.readFileSync(httpsOptions.ca);
69
+ server = createServer$1(httpsOptions, app.callback());
70
+ if (debugPort) debugPortServer = createServer(app.callback());
71
+ } else {
72
+ server = createServer(app.callback());
73
+ if (debugPort) debugPortServer = server;
74
+ }
75
+ server.once("error", (err$1) => {
76
+ consoleLogger.error("[app_worker] server got error: %s, code: %s", err$1.message, err$1.code);
77
+ exitProcess();
78
+ });
79
+ app.emit("server", server);
80
+ if (options.sticky && options.stickyWorkerPort) {
81
+ server.listen(options.stickyWorkerPort, "127.0.0.1");
82
+ io.on("message", (message, connection) => {
83
+ if (message !== "sticky-session:connection") return;
84
+ server.emit("connection", connection);
85
+ connection.resume();
86
+ });
87
+ } else {
88
+ if (listenConfig.path) server.listen(listenConfig.path);
89
+ else {
90
+ if (typeof port !== "number") {
91
+ consoleLogger.error("[app_worker:%s] port should be number, but got %s(%s)", process.pid, port, typeof port);
92
+ exitProcess();
93
+ return;
94
+ }
95
+ if (reusePort) {
96
+ const listenOptions = {
97
+ port,
98
+ reusePort
99
+ };
100
+ if (listenConfig.hostname) listenOptions.host = listenConfig.hostname;
101
+ debug("[app_worker:%s] listen with reusePort options %j", process.pid, listenOptions);
102
+ server.listen(listenOptions);
103
+ } else {
104
+ const args = [port];
105
+ if (listenConfig.hostname) args.push(listenConfig.hostname);
106
+ debug("listen options %j", args);
107
+ server.listen(...args);
108
+ }
109
+ }
110
+ if (debugPortServer) {
111
+ debug("listen on debug port: %s", debugPort);
112
+ debugPortServer.listen(debugPort);
113
+ }
114
+ }
115
+ server.once("listening", () => {
116
+ let address = server.address() || { port };
117
+ if (typeof address === "string") address = {
118
+ address,
119
+ addressType: -1
120
+ };
121
+ debug("[app_worker:%s] listening at %j, reusePort: %o", process.pid, address, reusePort);
122
+ io.send({
123
+ to: "master",
124
+ action: "app-start",
125
+ data: {
126
+ address,
127
+ workerId: io.workerId
128
+ },
129
+ reusePort
130
+ });
131
+ });
132
+ }
133
+ io.gracefulExit({
134
+ logger: consoleLogger,
135
+ label: "app_worker",
136
+ beforeExit: () => app.close()
137
+ });
138
+ }
139
+
140
+ //#endregion
141
+ export { startAppWorker };
@@ -0,0 +1,14 @@
1
+ import { AppWorkerIO } from "./app.js";
2
+
3
+ //#region src/worker_protocol/process.d.ts
4
+
5
+ /**
6
+ * Process-backed worker transport shared by the traditional process entries
7
+ * and the egg-bundler worker entry (plain boot or V8 snapshot restore).
8
+ *
9
+ * `node:cluster` is fetched only for the reusePort notification so snapshot
10
+ * construction does not cache the build process's primary/worker flavor.
11
+ */
12
+ declare function createProcessWorkerIO(): AppWorkerIO;
13
+ //#endregion
14
+ export { createProcessWorkerIO };
@@ -0,0 +1,38 @@
1
+ import { graceful } from "graceful-process";
2
+
3
+ //#region src/worker_protocol/process.ts
4
+ /**
5
+ * Process-backed worker transport shared by the traditional process entries
6
+ * and the egg-bundler worker entry (plain boot or V8 snapshot restore).
7
+ *
8
+ * `node:cluster` is fetched only for the reusePort notification so snapshot
9
+ * construction does not cache the build process's primary/worker flavor.
10
+ */
11
+ function createProcessWorkerIO() {
12
+ return {
13
+ get workerId() {
14
+ return process.pid;
15
+ },
16
+ send(message) {
17
+ message.senderWorkerId = String(process.pid);
18
+ if (message.action === "app-start" && message.reusePort) {
19
+ process.getBuiltinModule("node:cluster").worker.send(message);
20
+ return;
21
+ }
22
+ process.send(message);
23
+ },
24
+ on(event, listener) {
25
+ process.on(event, listener);
26
+ },
27
+ kill() {
28
+ process.exitCode = 1;
29
+ process.kill(process.pid);
30
+ },
31
+ gracefulExit(options) {
32
+ graceful(options);
33
+ }
34
+ };
35
+ }
36
+
37
+ //#endregion
38
+ export { createProcessWorkerIO };
@@ -0,0 +1,11 @@
1
+ import { AppWorkerIO } from "./app.js";
2
+
3
+ //#region src/worker_protocol/worker-thread.d.ts
4
+
5
+ /**
6
+ * Worker-thread-backed transport shared by the standard worker entries and
7
+ * generated bundle entries.
8
+ */
9
+ declare function createWorkerThreadIO(): AppWorkerIO;
10
+ //#endregion
11
+ export { createWorkerThreadIO };
@@ -0,0 +1,34 @@
1
+ import { parentPort, threadId } from "node:worker_threads";
2
+
3
+ //#region src/worker_protocol/worker-thread.ts
4
+ /**
5
+ * Worker-thread-backed transport shared by the standard worker entries and
6
+ * generated bundle entries.
7
+ */
8
+ function createWorkerThreadIO() {
9
+ const port = parentPort;
10
+ if (!port) throw new Error("createWorkerThreadIO() must be called inside a worker thread");
11
+ return {
12
+ workerId: threadId,
13
+ send(message) {
14
+ message.senderWorkerId = String(threadId);
15
+ port.postMessage(message);
16
+ },
17
+ on(event, listener) {
18
+ port.on(event, listener);
19
+ },
20
+ kill() {
21
+ process.exit(1);
22
+ },
23
+ gracefulExit(options) {
24
+ const { beforeExit } = options;
25
+ process.on("exit", async (code) => {
26
+ if (typeof beforeExit === "function") await beforeExit();
27
+ process.exit(code);
28
+ });
29
+ }
30
+ };
31
+ }
32
+
33
+ //#endregion
34
+ export { createWorkerThreadIO };
@@ -0,0 +1,5 @@
1
+ import { AgentWorkerIO, startAgentWorker } from "./worker_protocol/agent.js";
2
+ import { AppWorkerIO, AppWorkerProtocolOptions, startAppWorker } from "./worker_protocol/app.js";
3
+ import { createProcessWorkerIO } from "./worker_protocol/process.js";
4
+ import { createWorkerThreadIO } from "./worker_protocol/worker-thread.js";
5
+ export { type AgentWorkerIO, type AppWorkerIO, type AppWorkerProtocolOptions, createProcessWorkerIO, createWorkerThreadIO, startAgentWorker, startAppWorker };
@@ -0,0 +1,6 @@
1
+ import { startAgentWorker } from "./worker_protocol/agent.js";
2
+ import { createProcessWorkerIO } from "./worker_protocol/process.js";
3
+ import { createWorkerThreadIO } from "./worker_protocol/worker-thread.js";
4
+ import { startAppWorker } from "./worker_protocol/app.js";
5
+
6
+ export { createProcessWorkerIO, createWorkerThreadIO, startAgentWorker, startAppWorker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eggjs/cluster",
3
- "version": "4.0.2-beta.24",
3
+ "version": "4.0.2-beta.26",
4
4
  "description": "cluster manager for egg",
5
5
  "keywords": [
6
6
  "cluster",
@@ -29,6 +29,7 @@
29
29
  ".": "./dist/index.js",
30
30
  "./agent_worker": "./dist/agent_worker.js",
31
31
  "./app_worker": "./dist/app_worker.js",
32
+ "./worker_protocol": "./dist/worker_protocol.js",
32
33
  "./package.json": "./package.json"
33
34
  },
34
35
  "publishConfig": {
@@ -37,6 +38,7 @@
37
38
  ".": "./dist/index.js",
38
39
  "./agent_worker": "./dist/agent_worker.js",
39
40
  "./app_worker": "./dist/app_worker.js",
41
+ "./worker_protocol": "./dist/worker_protocol.js",
40
42
  "./package.json": "./package.json"
41
43
  }
42
44
  },
@@ -46,7 +48,7 @@
46
48
  "test": "vitest run"
47
49
  },
48
50
  "dependencies": {
49
- "@eggjs/utils": "5.0.2-beta.24",
51
+ "@eggjs/utils": "5.0.2-beta.26",
50
52
  "@fengmk2/ps-tree": "^2.0.1",
51
53
  "cfork": "^2.0.0",
52
54
  "cluster-reload": "^2.0.0",
@@ -59,10 +61,10 @@
59
61
  "utility": "^2.5.0"
60
62
  },
61
63
  "devDependencies": {
62
- "@eggjs/errors": "3.0.2-beta.24",
63
- "@eggjs/mock": "7.0.2-beta.24",
64
- "@eggjs/supertest": "9.0.2-beta.24",
65
- "@eggjs/tsconfig": "3.1.2-beta.24",
64
+ "@eggjs/errors": "3.0.2-beta.26",
65
+ "@eggjs/mock": "7.0.2-beta.26",
66
+ "@eggjs/supertest": "9.0.2-beta.26",
67
+ "@eggjs/tsconfig": "3.1.2-beta.26",
66
68
  "address": "2",
67
69
  "coffee": "5",
68
70
  "tsdown": "^0.18.2",