@eggjs/cluster 3.0.0-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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/dist/commonjs/agent_worker.d.ts +1 -0
  4. package/dist/commonjs/agent_worker.js +65 -0
  5. package/dist/commonjs/app_worker.d.ts +1 -0
  6. package/dist/commonjs/app_worker.js +151 -0
  7. package/dist/commonjs/dirname.d.ts +1 -0
  8. package/dist/commonjs/dirname.js +17 -0
  9. package/dist/commonjs/error/ClusterAgentWorkerError.d.ts +10 -0
  10. package/dist/commonjs/error/ClusterAgentWorkerError.js +23 -0
  11. package/dist/commonjs/error/ClusterWorkerExceptionError.d.ts +7 -0
  12. package/dist/commonjs/error/ClusterWorkerExceptionError.js +18 -0
  13. package/dist/commonjs/error/index.d.ts +2 -0
  14. package/dist/commonjs/error/index.js +19 -0
  15. package/dist/commonjs/index.d.ts +17 -0
  16. package/dist/commonjs/index.js +37 -0
  17. package/dist/commonjs/master.d.ts +90 -0
  18. package/dist/commonjs/master.js +544 -0
  19. package/dist/commonjs/package.json +3 -0
  20. package/dist/commonjs/utils/messenger.d.ts +87 -0
  21. package/dist/commonjs/utils/messenger.js +183 -0
  22. package/dist/commonjs/utils/mode/base/agent.d.ts +38 -0
  23. package/dist/commonjs/utils/mode/base/agent.js +68 -0
  24. package/dist/commonjs/utils/mode/base/app.d.ts +48 -0
  25. package/dist/commonjs/utils/mode/base/app.js +81 -0
  26. package/dist/commonjs/utils/mode/impl/process/agent.d.ts +18 -0
  27. package/dist/commonjs/utils/mode/impl/process/agent.js +107 -0
  28. package/dist/commonjs/utils/mode/impl/process/app.d.ts +20 -0
  29. package/dist/commonjs/utils/mode/impl/process/app.js +134 -0
  30. package/dist/commonjs/utils/mode/impl/worker_threads/agent.d.ts +18 -0
  31. package/dist/commonjs/utils/mode/impl/worker_threads/agent.js +90 -0
  32. package/dist/commonjs/utils/mode/impl/worker_threads/app.d.ts +25 -0
  33. package/dist/commonjs/utils/mode/impl/worker_threads/app.js +156 -0
  34. package/dist/commonjs/utils/options.d.ts +80 -0
  35. package/dist/commonjs/utils/options.js +81 -0
  36. package/dist/commonjs/utils/terminate.d.ts +6 -0
  37. package/dist/commonjs/utils/terminate.js +89 -0
  38. package/dist/commonjs/utils/worker_manager.d.ts +25 -0
  39. package/dist/commonjs/utils/worker_manager.js +76 -0
  40. package/dist/esm/agent_worker.d.ts +1 -0
  41. package/dist/esm/agent_worker.js +63 -0
  42. package/dist/esm/app_worker.d.ts +1 -0
  43. package/dist/esm/app_worker.js +146 -0
  44. package/dist/esm/dirname.d.ts +1 -0
  45. package/dist/esm/dirname.js +11 -0
  46. package/dist/esm/error/ClusterAgentWorkerError.d.ts +10 -0
  47. package/dist/esm/error/ClusterAgentWorkerError.js +19 -0
  48. package/dist/esm/error/ClusterWorkerExceptionError.d.ts +7 -0
  49. package/dist/esm/error/ClusterWorkerExceptionError.js +14 -0
  50. package/dist/esm/error/index.d.ts +2 -0
  51. package/dist/esm/error/index.js +3 -0
  52. package/dist/esm/index.d.ts +17 -0
  53. package/dist/esm/index.js +19 -0
  54. package/dist/esm/master.d.ts +90 -0
  55. package/dist/esm/master.js +537 -0
  56. package/dist/esm/package.json +3 -0
  57. package/dist/esm/utils/messenger.d.ts +87 -0
  58. package/dist/esm/utils/messenger.js +176 -0
  59. package/dist/esm/utils/mode/base/agent.d.ts +38 -0
  60. package/dist/esm/utils/mode/base/agent.js +60 -0
  61. package/dist/esm/utils/mode/base/app.d.ts +48 -0
  62. package/dist/esm/utils/mode/base/app.js +73 -0
  63. package/dist/esm/utils/mode/impl/process/agent.d.ts +18 -0
  64. package/dist/esm/utils/mode/impl/process/agent.js +102 -0
  65. package/dist/esm/utils/mode/impl/process/app.d.ts +20 -0
  66. package/dist/esm/utils/mode/impl/process/app.js +126 -0
  67. package/dist/esm/utils/mode/impl/worker_threads/agent.d.ts +18 -0
  68. package/dist/esm/utils/mode/impl/worker_threads/agent.js +82 -0
  69. package/dist/esm/utils/mode/impl/worker_threads/app.d.ts +25 -0
  70. package/dist/esm/utils/mode/impl/worker_threads/app.js +151 -0
  71. package/dist/esm/utils/options.d.ts +80 -0
  72. package/dist/esm/utils/options.js +75 -0
  73. package/dist/esm/utils/terminate.d.ts +6 -0
  74. package/dist/esm/utils/terminate.js +86 -0
  75. package/dist/esm/utils/worker_manager.d.ts +25 -0
  76. package/dist/esm/utils/worker_manager.js +72 -0
  77. package/dist/package.json +4 -0
  78. package/package.json +94 -0
  79. package/src/agent_worker.ts +75 -0
  80. package/src/app_worker.ts +170 -0
  81. package/src/dirname.ts +11 -0
  82. package/src/error/ClusterAgentWorkerError.ts +19 -0
  83. package/src/error/ClusterWorkerExceptionError.ts +17 -0
  84. package/src/error/index.ts +2 -0
  85. package/src/index.ts +26 -0
  86. package/src/master.ts +641 -0
  87. package/src/utils/messenger.ts +199 -0
  88. package/src/utils/mode/base/agent.ts +90 -0
  89. package/src/utils/mode/base/app.ts +115 -0
  90. package/src/utils/mode/impl/process/agent.ts +118 -0
  91. package/src/utils/mode/impl/process/app.ts +146 -0
  92. package/src/utils/mode/impl/worker_threads/agent.ts +98 -0
  93. package/src/utils/mode/impl/worker_threads/app.ts +180 -0
  94. package/src/utils/options.ts +169 -0
  95. package/src/utils/terminate.ts +97 -0
  96. package/src/utils/worker_manager.ts +87 -0
package/src/master.ts ADDED
@@ -0,0 +1,641 @@
1
+ import os from 'node:os';
2
+ import v8 from 'node:v8';
3
+ import util from 'node:util';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+ import net from 'node:net';
7
+ import { ReadyEventEmitter } from 'get-ready';
8
+ import { detectPort } from 'detect-port';
9
+ import { reload } from 'cluster-reload';
10
+ import { EggConsoleLogger as ConsoleLogger } from 'egg-logger';
11
+ import { readJSONSync } from 'utility';
12
+ import terminalLink from 'terminal-link';
13
+ import { parseOptions, ClusterOptions, ParsedClusterOptions } from './utils/options.js';
14
+ import { WorkerManager } from './utils/worker_manager.js';
15
+ import { Messenger } from './utils/messenger.js';
16
+ import {
17
+ AgentProcessWorker, AgentProcessUtils as ProcessAgentWorker,
18
+ } from './utils/mode/impl/process/agent.js';
19
+ import { AppProcessWorker, AppProcessUtils as ProcessAppWorker } from './utils/mode/impl/process/app.js';
20
+ import {
21
+ AgentThreadWorker, AgentThreadUtils as WorkerThreadsAgentWorker,
22
+ } from './utils/mode/impl/worker_threads/agent.js';
23
+ import { AppThreadWorker, AppThreadUtils as WorkerThreadsAppWorker } from './utils/mode/impl/worker_threads/app.js';
24
+ import { ClusterWorkerExceptionError } from './error/ClusterWorkerExceptionError.js';
25
+
26
+ export interface MasterOptions extends ParsedClusterOptions {
27
+ clusterPort?: number;
28
+ stickyWorkerPort?: number;
29
+ }
30
+
31
+ export class Master extends ReadyEventEmitter {
32
+ options: MasterOptions;
33
+ isStarted = false;
34
+ workerManager: WorkerManager;
35
+ messenger: Messenger;
36
+ isProduction: boolean;
37
+ agentWorkerIndex = 0;
38
+ closed = false;
39
+ logger: ConsoleLogger;
40
+ agentWorker: ProcessAgentWorker | WorkerThreadsAgentWorker;
41
+ appWorker: ProcessAppWorker | WorkerThreadsAppWorker;
42
+ #logMethod: 'info' | 'debug';
43
+ #realPort?: number;
44
+ #protocol: string;
45
+ #appAddress: string;
46
+
47
+ constructor(options?: ClusterOptions) {
48
+ super();
49
+ this.#start(options)
50
+ .catch(err => {
51
+ this.ready(err);
52
+ });
53
+ }
54
+
55
+ async #start(options?: ClusterOptions) {
56
+ this.options = await parseOptions(options);
57
+ this.workerManager = new WorkerManager();
58
+ this.messenger = new Messenger(this, this.workerManager);
59
+ this.isProduction = isProduction(this.options);
60
+ this.#realPort = this.options.port;
61
+ this.#protocol = this.options.https ? 'https' : 'http';
62
+
63
+ // app started or not
64
+ this.isStarted = false;
65
+ this.logger = new ConsoleLogger({ level: process.env.EGG_MASTER_LOGGER_LEVEL ?? 'INFO' });
66
+ this.#logMethod = 'info';
67
+ if (this.options.env === 'local' || process.env.NODE_ENV === 'development') {
68
+ this.#logMethod = 'debug';
69
+ }
70
+
71
+ // get the real framework info
72
+ const frameworkPath = this.options.framework;
73
+ const frameworkPkg = readJSONSync(path.join(frameworkPath, 'package.json'));
74
+
75
+ // set app & agent worker impl
76
+ if (this.options.startMode === 'worker_threads') {
77
+ this.startByWorkerThreads();
78
+ } else {
79
+ this.startByProcess();
80
+ }
81
+
82
+ this.log(`[master] =================== ${frameworkPkg.name} start =====================`);
83
+ this.logger.info(`[master] node version ${process.version}`);
84
+ /* istanbul ignore next */
85
+ if ('alinode' in process) {
86
+ this.logger.info(`[master] alinode version ${process.alinode}`);
87
+ }
88
+ this.logger.info(`[master] ${frameworkPkg.name} version ${frameworkPkg.version}`);
89
+
90
+ if (this.isProduction) {
91
+ this.logger.info('[master] start with options:%s%s',
92
+ os.EOL, JSON.stringify(this.options, null, 2));
93
+ } else {
94
+ this.log('[master] start with options: %j', this.options);
95
+ }
96
+ this.log('[master] start with env: isProduction: %s, EGG_SERVER_ENV: %s, NODE_ENV: %s',
97
+ this.isProduction, this.options.env, process.env.NODE_ENV);
98
+
99
+ const startTime = Date.now();
100
+
101
+ this.ready(() => {
102
+ this.isStarted = true;
103
+ const stickyMsg = this.options.sticky ? ' with STICKY MODE!' : '';
104
+ const startedURL = terminalLink(this.#appAddress, this.#appAddress, { fallback: false });
105
+ this.logger.info('[master] %s started on %s (%sms)%s',
106
+ frameworkPkg.name, startedURL, Date.now() - startTime, stickyMsg);
107
+ if (this.options.debugPort) {
108
+ const url = getAddress({
109
+ port: this.options.debugPort,
110
+ protocol: 'http',
111
+ });
112
+ const debugPortURL = terminalLink(url, url, { fallback: false });
113
+ this.logger.info('[master] %s started debug port on %s', frameworkPkg.name, debugPortURL);
114
+ }
115
+
116
+ const action = 'egg-ready';
117
+ this.messenger.send({
118
+ action,
119
+ to: 'parent',
120
+ data: {
121
+ port: this.#realPort,
122
+ debugPort: this.options.debugPort,
123
+ address: this.#appAddress,
124
+ protocol: this.#protocol,
125
+ },
126
+ });
127
+ this.messenger.send({
128
+ action,
129
+ to: 'app',
130
+ data: this.options,
131
+ });
132
+ this.messenger.send({
133
+ action,
134
+ to: 'agent',
135
+ data: this.options,
136
+ });
137
+
138
+ // start check agent and worker status
139
+ if (this.isProduction) {
140
+ this.workerManager.startCheck();
141
+ }
142
+ });
143
+
144
+ this.on('agent-exit', this.onAgentExit.bind(this));
145
+ this.on('agent-start', this.onAgentStart.bind(this));
146
+ this.on('app-exit', this.onAppExit.bind(this));
147
+ this.on('app-start', this.onAppStart.bind(this));
148
+ this.on('reload-worker', this.onReload.bind(this));
149
+
150
+
151
+ // fork app workers after agent started
152
+ this.once('agent-start', this.forkAppWorkers.bind(this));
153
+ // get the real port from options and app.config
154
+ // app worker will send after loading
155
+ this.on('realport', ({ port, protocol }) => {
156
+ if (port) {
157
+ this.#realPort = port;
158
+ }
159
+ if (protocol) {
160
+ this.#protocol = protocol;
161
+ }
162
+ });
163
+
164
+ // https://nodejs.org/api/process.html#process_signal_events
165
+ // https://en.wikipedia.org/wiki/Unix_signal
166
+ // kill(2) Ctrl-C
167
+ process.once('SIGINT', this.onSignal.bind(this, 'SIGINT'));
168
+ // kill(3) Ctrl-\
169
+ process.once('SIGQUIT', this.onSignal.bind(this, 'SIGQUIT'));
170
+ // kill(15) default
171
+ process.once('SIGTERM', this.onSignal.bind(this, 'SIGTERM'));
172
+
173
+ process.once('exit', this.onExit.bind(this));
174
+
175
+ // write pid to file if provided
176
+ if (this.options.pidFile) {
177
+ fs.mkdirSync(path.dirname(this.options.pidFile), { recursive: true });
178
+ fs.writeFileSync(this.options.pidFile, process.pid.toString(), 'utf-8');
179
+ }
180
+
181
+ this.detectPorts()
182
+ .then(() => {
183
+ this.forkAgentWorker();
184
+ });
185
+
186
+ // exit when agent or worker exception
187
+ this.workerManager.on('exception', (count: {
188
+ agent: number;
189
+ worker: number;
190
+ }) => {
191
+ const err = new ClusterWorkerExceptionError(count.agent, count.worker);
192
+ this.logger.error(err);
193
+ process.exit(1);
194
+ });
195
+ }
196
+
197
+ startByProcess() {
198
+ this.agentWorker = new ProcessAgentWorker(this.options, {
199
+ log: this.log.bind(this),
200
+ logger: this.logger,
201
+ messenger: this.messenger,
202
+ });
203
+
204
+ this.appWorker = new ProcessAppWorker(this.options, {
205
+ log: this.log.bind(this),
206
+ logger: this.logger,
207
+ messenger: this.messenger,
208
+ isProduction: this.isProduction,
209
+ });
210
+ }
211
+
212
+ startByWorkerThreads() {
213
+ this.agentWorker = new WorkerThreadsAgentWorker(this.options, {
214
+ log: this.log.bind(this),
215
+ logger: this.logger,
216
+ messenger: this.messenger,
217
+ });
218
+
219
+ this.appWorker = new WorkerThreadsAppWorker(this.options, {
220
+ log: this.log.bind(this),
221
+ logger: this.logger,
222
+ messenger: this.messenger,
223
+ isProduction: this.isProduction,
224
+ });
225
+ }
226
+
227
+ async detectPorts() {
228
+ // Detect cluster client port
229
+ try {
230
+ const clusterPort = await detectPort();
231
+ this.options.clusterPort = clusterPort;
232
+ // If sticky mode, detect worker port
233
+ if (this.options.sticky) {
234
+ const stickyWorkerPort = await detectPort();
235
+ this.options.stickyWorkerPort = stickyWorkerPort;
236
+ }
237
+ } catch (err) {
238
+ this.logger.error(err);
239
+ process.exit(1);
240
+ }
241
+ }
242
+
243
+ log(msg: string, ...args: any[]) {
244
+ this.logger[this.#logMethod](msg, ...args);
245
+ }
246
+
247
+ startMasterSocketServer(cb: (err?: Error) => void) {
248
+ // Create the outside facing server listening on our port.
249
+ net.createServer({
250
+ pauseOnConnect: true,
251
+ }, connection => {
252
+ // We received a connection and need to pass it to the appropriate
253
+ // worker. Get the worker for this connection's source IP and pass
254
+ // it the connection.
255
+
256
+ /* istanbul ignore next */
257
+ if (!connection.remoteAddress) {
258
+ // This will happen when a client sends an RST(which is set to 1) right
259
+ // after the three-way handshake to the server.
260
+ // Read https://en.wikipedia.org/wiki/TCP_reset_attack for more details.
261
+ connection.destroy();
262
+ } else {
263
+ const worker = this.stickyWorker(connection.remoteAddress) as AppProcessWorker;
264
+ worker.instance.send('sticky-session:connection', connection);
265
+ }
266
+ }).listen(this.#realPort, cb);
267
+ }
268
+
269
+ stickyWorker(ip: string) {
270
+ const workerNumbers = this.options.workers;
271
+ const ws = this.workerManager.listWorkerIds();
272
+
273
+ let s = '';
274
+ for (let i = 0; i < ip.length; i++) {
275
+ if (!isNaN(parseInt(ip[i]))) {
276
+ s += ip[i];
277
+ }
278
+ }
279
+ const pid = ws[Number(s) % workerNumbers];
280
+ return this.workerManager.getWorker(pid)!;
281
+ }
282
+
283
+ forkAgentWorker() {
284
+ this.agentWorker.on('agent_forked', (agent: AgentProcessWorker | AgentThreadWorker) => {
285
+ this.workerManager.setAgent(agent);
286
+ });
287
+ this.agentWorker.fork();
288
+ }
289
+
290
+ forkAppWorkers() {
291
+ this.appWorker.on('worker_forked', (worker: AppProcessWorker | AppThreadWorker) => {
292
+ this.workerManager.setWorker(worker);
293
+ });
294
+ this.appWorker.fork();
295
+ }
296
+
297
+ /**
298
+ * close agent worker, App Worker will closed by cluster
299
+ *
300
+ * https://www.exratione.com/2013/05/die-child-process-die/
301
+ * make sure Agent Worker exit before master exit
302
+ *
303
+ * @param {number} timeout - kill agent timeout
304
+ * @return {Promise} -
305
+ */
306
+ async killAgentWorker(timeout: number) {
307
+ await this.agentWorker.kill(timeout);
308
+ }
309
+
310
+ async killAppWorkers(timeout: number) {
311
+ await this.appWorker.kill(timeout);
312
+ }
313
+
314
+ /**
315
+ * Agent Worker exit handler
316
+ * Will exit during startup, and refork during running.
317
+ */
318
+ onAgentExit(data: {
319
+ /** exit code */
320
+ code: number;
321
+ /** received signal */
322
+ signal: string;
323
+ }) {
324
+ if (this.closed) return;
325
+
326
+ this.messenger.send({
327
+ action: 'egg-pids',
328
+ to: 'app',
329
+ data: [],
330
+ });
331
+ const agentWorker = this.agentWorker;
332
+ this.workerManager.deleteAgent();
333
+
334
+ const err = new Error(util.format('[master] agent_worker#%s:%s died (code: %s, signal: %s)',
335
+ agentWorker.instance.id, agentWorker.instance.workerId, data.code, data.signal));
336
+ err.name = 'AgentWorkerDiedError';
337
+ this.logger.error(err);
338
+
339
+ // remove all listeners to avoid memory leak
340
+ agentWorker.clean();
341
+
342
+ if (this.isStarted) {
343
+ this.log('[master] try to start a new agent_worker after 1s ...');
344
+ setTimeout(() => {
345
+ this.logger.info('[master] new agent_worker starting...');
346
+ this.forkAgentWorker();
347
+ }, 1000);
348
+ this.messenger.send({
349
+ action: 'agent-worker-died',
350
+ to: 'parent',
351
+ });
352
+ } else {
353
+ this.logger.error('[master] agent_worker#%s:%s start fail, exiting with code:1',
354
+ agentWorker.instance.id, agentWorker.instance.workerId);
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ onAgentStart() {
360
+ this.agentWorker.instance.status = 'started';
361
+
362
+ // Send egg-ready when agent is started after launched
363
+ if (this.appWorker.isAllWorkerStarted) {
364
+ this.messenger.send({
365
+ action: 'egg-ready',
366
+ to: 'agent',
367
+ data: this.options,
368
+ });
369
+ }
370
+
371
+ this.messenger.send({
372
+ action: 'egg-pids',
373
+ to: 'app',
374
+ data: [ this.agentWorker.instance.workerId ],
375
+ });
376
+ // should send current worker pids when agent restart
377
+ if (this.isStarted) {
378
+ this.messenger.send({
379
+ action: 'egg-pids',
380
+ to: 'agent',
381
+ data: this.workerManager.getListeningWorkerIds(),
382
+ });
383
+ }
384
+
385
+ this.messenger.send({
386
+ action: 'agent-start',
387
+ to: 'app',
388
+ });
389
+ this.logger.info('[master] agent_worker#%s:%s started (%sms)',
390
+ this.agentWorker.instance.id, this.agentWorker.instance.workerId, Date.now() - this.agentWorker.startTime);
391
+ }
392
+
393
+ /**
394
+ * App Worker exit handler
395
+ */
396
+ onAppExit(data: {
397
+ workerId: number;
398
+ code: number;
399
+ signal: string;
400
+ }) {
401
+ if (this.closed) return;
402
+
403
+ const worker = this.workerManager.getWorker(data.workerId)!;
404
+ if (!worker.isDevReload) {
405
+ const signal = data.signal;
406
+ const message = util.format(
407
+ '[master] app_worker#%s:%s died (code: %s, signal: %s, suicide: %s, state: %s), current workers: %j',
408
+ worker.id, worker.workerId, worker.exitCode, signal,
409
+ worker.exitedAfterDisconnect, worker.state,
410
+ this.workerManager.listWorkerIds(),
411
+ );
412
+ if (this.options.isDebug && signal === 'SIGKILL') {
413
+ // exit if died during debug
414
+ this.logger.error(message);
415
+ this.logger.error('[master] worker kill by debugger, exiting...');
416
+ setTimeout(() => this.close(), 10);
417
+ } else {
418
+ const err = new Error(message);
419
+ err.name = 'AppWorkerDiedError';
420
+ this.logger.error(err);
421
+ }
422
+ }
423
+
424
+ // remove all listeners to avoid memory leak
425
+ worker.clean();
426
+ this.workerManager.deleteWorker(data.workerId);
427
+ // send message to agent with alive workers
428
+ this.messenger.send({
429
+ action: 'egg-pids',
430
+ to: 'agent',
431
+ data: this.workerManager.getListeningWorkerIds(),
432
+ });
433
+
434
+ if (this.appWorker.isAllWorkerStarted) {
435
+ // cfork will only refork at production mode
436
+ this.messenger.send({
437
+ action: 'app-worker-died',
438
+ to: 'parent',
439
+ });
440
+ } else {
441
+ // exit if died during startup
442
+ this.logger.error('[master] app_worker#%s:%s start fail, exiting with code:1',
443
+ worker.id, worker.workerId);
444
+ process.exit(1);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * after app worker
450
+ */
451
+ onAppStart(data: {
452
+ workerId: number;
453
+ address: ListeningAddress;
454
+ }) {
455
+ const address = data.address;
456
+
457
+ // worker should listen stickyWorkerPort when sticky mode
458
+ if (this.options.sticky) {
459
+ if (String(address.port) !== String(this.options.stickyWorkerPort)) {
460
+ return;
461
+ }
462
+ // worker should listen REALPORT when not sticky mode
463
+ } else if (this.options.startMode !== 'worker_threads' &&
464
+ !isUnixSock(address) &&
465
+ (String(address.port) !== String(this.#realPort))) {
466
+ return;
467
+ }
468
+
469
+ // send message to agent with alive workers
470
+ this.messenger.send({
471
+ action: 'egg-pids',
472
+ to: 'agent',
473
+ data: this.workerManager.getListeningWorkerIds(),
474
+ });
475
+
476
+ this.appWorker.startSuccessCount++;
477
+
478
+ const worker = this.workerManager.getWorker(data.workerId)!;
479
+ const remain = this.appWorker.isAllWorkerStarted ? 0 : this.options.workers - this.appWorker.startSuccessCount;
480
+ this.log('[master] app_worker#%s:%s started at %s, remain %s (%sms)',
481
+ worker.id, worker.workerId, address.port, remain,
482
+ Date.now() - this.appWorker.startTime);
483
+
484
+ // Send egg-ready when app is started after launched
485
+ if (this.appWorker.isAllWorkerStarted) {
486
+ this.messenger.send({
487
+ action: 'egg-ready',
488
+ to: 'app',
489
+ data: this.options,
490
+ });
491
+ }
492
+
493
+ // if app is started, it should enable this worker
494
+ if (this.appWorker.isAllWorkerStarted) {
495
+ worker.disableRefork = false;
496
+ }
497
+
498
+ if (this.appWorker.isAllWorkerStarted || this.appWorker.startSuccessCount < this.options.workers) {
499
+ return;
500
+ }
501
+
502
+ this.appWorker.isAllWorkerStarted = true;
503
+
504
+ // enable all workers when app started
505
+ for (const worker of this.workerManager.listWorkers()) {
506
+ worker.disableRefork = false;
507
+ }
508
+
509
+ address.protocol = this.#protocol;
510
+ address.port = this.options.sticky ? this.#realPort! : address.port;
511
+ this.#appAddress = getAddress(address);
512
+
513
+ if (this.options.sticky) {
514
+ this.startMasterSocketServer(err => {
515
+ if (err) {
516
+ return this.ready(err);
517
+ }
518
+ this.ready(true);
519
+ });
520
+ } else {
521
+ this.ready(true);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * master exit handler
527
+ */
528
+ onExit(code: number) {
529
+ if (this.options.pidFile && fs.existsSync(this.options.pidFile)) {
530
+ try {
531
+ fs.unlinkSync(this.options.pidFile);
532
+ } catch (err: any) {
533
+ /* istanbul ignore next */
534
+ this.logger.error('[master] delete pidFile %s fail with %s', this.options.pidFile, err.message);
535
+ }
536
+ }
537
+ // istanbul can't cover here
538
+ // https://github.com/gotwarlost/istanbul/issues/567
539
+ const level = code === 0 ? 'info' : 'error';
540
+ this.logger[level]('[master] exit with code:%s', code);
541
+ }
542
+
543
+ onSignal(signal: string) {
544
+ if (this.closed) return;
545
+
546
+ this.logger.info('[master] master is killed by signal %s, closing', signal);
547
+ // logger more info
548
+ const { used_heap_size, heap_size_limit } = v8.getHeapStatistics();
549
+ this.logger.info('[master] system memory: total %s, free %s', os.totalmem(), os.freemem());
550
+ this.logger.info('[master] process info: heap_limit %s, heap_used %s', heap_size_limit, used_heap_size);
551
+
552
+ this.close();
553
+ }
554
+
555
+ /**
556
+ * reload workers, for develop purpose
557
+ */
558
+ onReload() {
559
+ this.log('[master] reload %s workers...', this.options.workers);
560
+ for (const worker of this.workerManager.listWorkers()) {
561
+ worker.isDevReload = true;
562
+ }
563
+ reload(this.options.workers);
564
+ }
565
+
566
+ async close() {
567
+ this.closed = true;
568
+ try {
569
+ await this._doClose();
570
+ this.log('[master] close done, exiting with code:0');
571
+ process.exit(0);
572
+ } catch (e) {
573
+ this.logger.error('[master] close with error: ', e);
574
+ process.exit(1);
575
+ }
576
+ }
577
+
578
+ async _doClose() {
579
+ // kill app workers
580
+ // kill agent worker
581
+ // exit itself
582
+ const legacyTimeout = process.env.EGG_MASTER_CLOSE_TIMEOUT || '5000';
583
+ const appTimeout = parseInt(process.env.EGG_APP_CLOSE_TIMEOUT || legacyTimeout);
584
+ const agentTimeout = parseInt(process.env.EGG_AGENT_CLOSE_TIMEOUT || legacyTimeout);
585
+ this.logger.info('[master] send kill SIGTERM to app workers, will exit with code:0 after %sms', appTimeout);
586
+ this.logger.info('[master] wait %sms', appTimeout);
587
+ try {
588
+ await this.killAppWorkers(appTimeout);
589
+ } catch (e) {
590
+ this.logger.error('[master] app workers exit error: ', e);
591
+ }
592
+ this.logger.info('[master] send kill SIGTERM to agent worker, will exit with code:0 after %sms', agentTimeout);
593
+ this.logger.info('[master] wait %sms', agentTimeout);
594
+ try {
595
+ await this.killAgentWorker(agentTimeout);
596
+ } catch (e) /* istanbul ignore next */ {
597
+ this.logger.error('[master] agent worker exit error: ', e);
598
+ }
599
+ }
600
+ }
601
+
602
+ function isProduction(options: ClusterOptions) {
603
+ if (options.env) {
604
+ return options.env !== 'local' && options.env !== 'unittest';
605
+ }
606
+ return process.env.NODE_ENV === 'production';
607
+ }
608
+
609
+ interface ListeningAddress {
610
+ port: number;
611
+ protocol: string;
612
+ address?: string;
613
+ // https://nodejs.org/api/cluster.html#cluster_event_listening_1
614
+ addressType?: number;
615
+ }
616
+
617
+ function getAddress({
618
+ addressType,
619
+ address,
620
+ port,
621
+ protocol,
622
+ }: ListeningAddress) {
623
+ // unix sock
624
+ // https://nodejs.org/api/cluster.html#cluster_event_listening_1
625
+ if (addressType === -1) {
626
+ return address!;
627
+ }
628
+
629
+ let hostname = address;
630
+ if (!hostname && process.env.HOST && process.env.HOST !== '0.0.0.0') {
631
+ hostname = process.env.HOST;
632
+ }
633
+ if (!hostname) {
634
+ hostname = '127.0.0.1';
635
+ }
636
+ return `${protocol}://${hostname}:${port}`;
637
+ }
638
+
639
+ function isUnixSock(address: ListeningAddress) {
640
+ return address.addressType === -1;
641
+ }