@maiyunnet/kebab 9.17.1 → 9.17.3
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.
- package/doc/kebab-rag.md +182 -100
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +10 -0
- package/lib/core.js +51 -0
- package/lib/socket.js +46 -19
- package/lib/undici/response.d.ts +1 -1
- package/lib/undici.d.ts +2 -0
- package/lib/undici.js +13 -2
- package/lib/ws.d.ts +15 -0
- package/lib/ws.js +103 -33
- package/package.json +2 -2
- package/sys/child.js +55 -41
- package/sys/master.js +90 -7
- package/www/example/ctr/test.d.ts +1 -0
- package/www/example/ctr/test.js +26 -2
package/sys/child.js
CHANGED
|
@@ -47,6 +47,10 @@ let http2Server;
|
|
|
47
47
|
const linkCount = {};
|
|
48
48
|
/** --- 是否正在停止,停止时对 HTTP/1.1 响应追加 Connection: close,避免请求完成后连接回到保活池 --- */
|
|
49
49
|
let stopping = false;
|
|
50
|
+
/** --- 当前缓动停止任务,避免重复 IPC 消息触发停止流程 --- */
|
|
51
|
+
let stopPromise = null;
|
|
52
|
+
/** --- 缓动停止是否必须一直等到现有连接全部结束 --- */
|
|
53
|
+
let stopWaitForever = false;
|
|
50
54
|
/** --- 当前活跃的 HTTP/2 会话集合,用于 graceful shutdown 时发送 GOAWAY 帧 --- */
|
|
51
55
|
const http2Sessions = new Set();
|
|
52
56
|
/**
|
|
@@ -552,56 +556,66 @@ async function reloadCert() {
|
|
|
552
556
|
defaultSc = certList.size > 0 ? certList.values().next().value.sc : undefined;
|
|
553
557
|
certHostIndex = {};
|
|
554
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* --- 停止接收新连接,并在现有请求、上传、异步任务和 WebSocket 全部结束后退出 worker ---
|
|
561
|
+
* @param waitForever 是否取消原有的一小时退出兜底,仅完整 stop 命令使用
|
|
562
|
+
* @returns worker 退出时结束
|
|
563
|
+
*/
|
|
564
|
+
async function gracefulStop(waitForever = false) {
|
|
565
|
+
if (waitForever) {
|
|
566
|
+
stopWaitForever = true;
|
|
567
|
+
}
|
|
568
|
+
if (stopPromise) {
|
|
569
|
+
return stopPromise;
|
|
570
|
+
}
|
|
571
|
+
stopping = true;
|
|
572
|
+
stopPromise = (async () => {
|
|
573
|
+
// --- 向所有 HTTP/2 会话发送 GOAWAY 帧,通知客户端停止在该连接上发送新请求并切换到新连接 ---
|
|
574
|
+
for (const session of http2Sessions) {
|
|
575
|
+
try {
|
|
576
|
+
session.goaway(http2.constants.NGHTTP2_NO_ERROR);
|
|
577
|
+
}
|
|
578
|
+
catch { }
|
|
579
|
+
}
|
|
580
|
+
httpServer?.close();
|
|
581
|
+
http2Server?.close();
|
|
582
|
+
// --- 立即关闭空闲保活连接,活跃连接会继续执行并在响应后关闭 ---
|
|
583
|
+
httpServer?.closeIdleConnections();
|
|
584
|
+
clearInterval(hbTimer);
|
|
585
|
+
sMonitor.stop();
|
|
586
|
+
// --- 等待活跃请求全部完成;WebSocket 的路由任务会一直保留到连接关闭 ---
|
|
587
|
+
/** --- 当前已等待时间,restart 最多等待一小时,完整 stop 不限制 --- */
|
|
588
|
+
let waiting = 0;
|
|
589
|
+
while (Object.keys(linkCount).length) {
|
|
590
|
+
const str = [];
|
|
591
|
+
for (const key in linkCount) {
|
|
592
|
+
str.push(key + ':' + linkCount[key].toString());
|
|
593
|
+
}
|
|
594
|
+
lCore.debug(`[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`);
|
|
595
|
+
lCore.log({}, `[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`, '-warning');
|
|
596
|
+
await lCore.sleep(5_000);
|
|
597
|
+
waiting += 5_000;
|
|
598
|
+
// --- 再次清理已变为空闲的保活连接 ---
|
|
599
|
+
httpServer?.closeIdleConnections();
|
|
600
|
+
if (!stopWaitForever && (waiting > 3_600_000)) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
process.exit();
|
|
605
|
+
})();
|
|
606
|
+
return stopPromise;
|
|
607
|
+
}
|
|
555
608
|
// --- 接收主进程回传信号,主要用来 reload,restart ---
|
|
556
609
|
process.on('message', function (msg) {
|
|
557
610
|
(async function () {
|
|
558
611
|
switch (msg.action) {
|
|
559
612
|
case 'reload': {
|
|
560
613
|
await reload();
|
|
561
|
-
|
|
562
|
-
console.log(`[child] Worker ${process.pid} reload execution succeeded.`);
|
|
614
|
+
lCore.display(`[CHILD] Worker ${process.pid} reload execution succeeded.`);
|
|
563
615
|
break;
|
|
564
616
|
}
|
|
565
617
|
case 'stop': {
|
|
566
|
-
|
|
567
|
-
stopping = true;
|
|
568
|
-
// --- 向所有 HTTP/2 会话发送 GOAWAY 帧,通知 CDN 客户端停止在该连接上发送新请求并切换到新连接 ---
|
|
569
|
-
for (const session of http2Sessions) {
|
|
570
|
-
try {
|
|
571
|
-
session.goaway(http2.constants.NGHTTP2_NO_ERROR);
|
|
572
|
-
}
|
|
573
|
-
catch { }
|
|
574
|
-
}
|
|
575
|
-
httpServer?.close();
|
|
576
|
-
http2Server?.close();
|
|
577
|
-
// --- 立即关闭空闲保活连接(无活跃请求的 keep-alive socket),避免进程长时间等待 ---
|
|
578
|
-
httpServer?.closeIdleConnections();
|
|
579
|
-
clearInterval(hbTimer);
|
|
580
|
-
sMonitor.stop();
|
|
581
|
-
// --- 等待活跃请求全部完成 ---
|
|
582
|
-
/** --- 当前已等待时间,等待不超过 1 小时 --- */
|
|
583
|
-
let waiting = 0;
|
|
584
|
-
while (true) {
|
|
585
|
-
if (!Object.keys(linkCount).length) {
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
// --- 有活跃连接,等待中 ---
|
|
589
|
-
const str = [];
|
|
590
|
-
for (const key in linkCount) {
|
|
591
|
-
str.push(key + ':' + linkCount[key].toString());
|
|
592
|
-
}
|
|
593
|
-
lCore.debug(`[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`);
|
|
594
|
-
lCore.log({}, `[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`, '-warning');
|
|
595
|
-
await lCore.sleep(5_000);
|
|
596
|
-
waiting += 5_000;
|
|
597
|
-
// --- 再次清理已变为空闲的保活连接 ---
|
|
598
|
-
httpServer?.closeIdleConnections();
|
|
599
|
-
if (waiting > 3600_000) {
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
// --- 链接全部断开 ---
|
|
604
|
-
process.exit();
|
|
618
|
+
await gracefulStop(msg.waitForever === true);
|
|
605
619
|
break;
|
|
606
620
|
}
|
|
607
621
|
case 'global': {
|
package/sys/master.js
CHANGED
|
@@ -19,6 +19,10 @@ import * as lTime from '#kebab/lib/time.js';
|
|
|
19
19
|
import * as lZip from '#kebab/lib/zip.js';
|
|
20
20
|
/** --- 当前运行中的子进程列表 --- */
|
|
21
21
|
const workerList = {};
|
|
22
|
+
/** --- master 是否正在缓动停止 --- */
|
|
23
|
+
let stopping = false;
|
|
24
|
+
/** --- 当前缓动停止任务,避免重复 stop 命令触发 --- */
|
|
25
|
+
let stopPromise = null;
|
|
22
26
|
/**
|
|
23
27
|
* --- 判断候选路径是否位于指定根目录内 ---
|
|
24
28
|
* @param rootPath 已解析的真实根目录
|
|
@@ -246,6 +250,12 @@ function createRpcListener() {
|
|
|
246
250
|
await restartWorkers();
|
|
247
251
|
break;
|
|
248
252
|
}
|
|
253
|
+
case 'stop': {
|
|
254
|
+
gracefulStop().catch((e) => {
|
|
255
|
+
lCore.display('[master] [gracefulStop]', e);
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
249
259
|
case 'global': {
|
|
250
260
|
// --- 为所有子进程更新 global 变量 ---
|
|
251
261
|
for (const pid in workerList) {
|
|
@@ -264,7 +274,7 @@ function createRpcListener() {
|
|
|
264
274
|
break;
|
|
265
275
|
}
|
|
266
276
|
case 'pm2': {
|
|
267
|
-
// ---
|
|
277
|
+
// --- 接收 PM2 操作 ---
|
|
268
278
|
if (!msg.name || typeof msg.name !== 'string') {
|
|
269
279
|
res.end('Invalid name');
|
|
270
280
|
return;
|
|
@@ -279,12 +289,18 @@ function createRpcListener() {
|
|
|
279
289
|
res.end('Invalid pm2Action');
|
|
280
290
|
return;
|
|
281
291
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
// --- 先确认已接收,让调用方完成当前 HTTP 响应,再执行可能终止本进程的 PM2 操作 ---
|
|
293
|
+
res.end('Done');
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
lCore.exec(`pm2 ${msg.pm2Action} ${msg.name}`).then((rtn) => {
|
|
296
|
+
if (rtn === false) {
|
|
297
|
+
lCore.display(`[master] PM2 ${msg.pm2Action} ${msg.name} failed.`);
|
|
298
|
+
}
|
|
299
|
+
}).catch((e) => {
|
|
300
|
+
lCore.display(`[master] PM2 ${msg.pm2Action} ${msg.name} failed.`, e);
|
|
301
|
+
});
|
|
302
|
+
}, 1_000);
|
|
303
|
+
return;
|
|
288
304
|
}
|
|
289
305
|
case 'npm': {
|
|
290
306
|
// --- 执行 npm install 操作 ---
|
|
@@ -931,10 +947,59 @@ function createRpcListener() {
|
|
|
931
947
|
});
|
|
932
948
|
}).listen(lCore.globalConfig.rpcPort);
|
|
933
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* --- 通知 worker 停止,并等待其现有连接全部结束后退出 ---
|
|
952
|
+
* @param worker 要停止的 worker
|
|
953
|
+
* @returns worker 退出后完成
|
|
954
|
+
*/
|
|
955
|
+
function stopWorker(worker) {
|
|
956
|
+
if (worker.isDead()) {
|
|
957
|
+
return Promise.resolve();
|
|
958
|
+
}
|
|
959
|
+
return new Promise((resolve) => {
|
|
960
|
+
worker.once('exit', () => { resolve(); });
|
|
961
|
+
try {
|
|
962
|
+
if (worker.isConnected()) {
|
|
963
|
+
worker.send({
|
|
964
|
+
'action': 'stop',
|
|
965
|
+
'waitForever': true,
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
worker.process.kill('SIGTERM');
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
lCore.display(`[master] Failed to notify worker ${worker.process.pid ?? 'undefined'} to stop, using SIGTERM.`, e);
|
|
973
|
+
worker.process.kill('SIGTERM');
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* --- 停止命令入口和所有 worker,等待整套 Kebab 进程排空后退出 master ---
|
|
979
|
+
* @returns 全部 worker 退出后完成
|
|
980
|
+
*/
|
|
981
|
+
async function gracefulStop() {
|
|
982
|
+
if (stopPromise) {
|
|
983
|
+
return stopPromise;
|
|
984
|
+
}
|
|
985
|
+
stopping = true;
|
|
986
|
+
stopPromise = (async () => {
|
|
987
|
+
lCore.display('[master] Graceful stop started.');
|
|
988
|
+
// --- 包括 restart 时已从 workerList 移除、但仍在排空的旧 worker ---
|
|
989
|
+
const workers = Object.values(cluster.workers ?? {}).filter((worker) => worker !== undefined);
|
|
990
|
+
await Promise.all(workers.map(worker => stopWorker(worker)));
|
|
991
|
+
lCore.display('[master] Graceful stop completed.');
|
|
992
|
+
process.exit(0);
|
|
993
|
+
})();
|
|
994
|
+
return stopPromise;
|
|
995
|
+
}
|
|
934
996
|
/**
|
|
935
997
|
* --- 零宕机重启所有 worker 进程:先创建新进程并等待监听就绪,再停止旧进程 ---
|
|
936
998
|
*/
|
|
937
999
|
async function restartWorkers() {
|
|
1000
|
+
if (stopping) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
938
1003
|
// --- 收集旧进程信息(先快照,避免迭代过程中修改 workerList) ---
|
|
939
1004
|
const oldEntries = [];
|
|
940
1005
|
for (const pid in workerList) {
|
|
@@ -946,6 +1011,9 @@ async function restartWorkers() {
|
|
|
946
1011
|
}
|
|
947
1012
|
// --- 阶段 1:创建新进程并等待其监听端口就绪 ---
|
|
948
1013
|
for (const entry of oldEntries) {
|
|
1014
|
+
if (stopping) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
949
1017
|
const newWorker = await createChildProcess(entry.cpu);
|
|
950
1018
|
if (newWorker) {
|
|
951
1019
|
await waitForWorkerListening(newWorker);
|
|
@@ -953,6 +1021,9 @@ async function restartWorkers() {
|
|
|
953
1021
|
}
|
|
954
1022
|
// --- 阶段 2:停止旧进程 ---
|
|
955
1023
|
for (const entry of oldEntries) {
|
|
1024
|
+
if (stopping) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
956
1027
|
entry.worker.send({
|
|
957
1028
|
'action': 'stop'
|
|
958
1029
|
});
|
|
@@ -963,6 +1034,9 @@ async function restartWorkers() {
|
|
|
963
1034
|
* --- 检测是否有死掉丢失的子进程,复活之 ---
|
|
964
1035
|
*/
|
|
965
1036
|
async function checkWorkerLost() {
|
|
1037
|
+
if (stopping) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
966
1040
|
const now = Date.now();
|
|
967
1041
|
for (const pid in workerList) {
|
|
968
1042
|
if (now - workerList[pid].hbtime < 30000) {
|
|
@@ -984,6 +1058,9 @@ async function checkWorkerLost() {
|
|
|
984
1058
|
* @param cpu CPU ID
|
|
985
1059
|
*/
|
|
986
1060
|
async function createChildProcess(cpu) {
|
|
1061
|
+
if (stopping) {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
987
1064
|
const worker = cluster.fork();
|
|
988
1065
|
if (!worker.process.pid) {
|
|
989
1066
|
return null;
|
|
@@ -1017,6 +1094,12 @@ async function createChildProcess(cpu) {
|
|
|
1017
1094
|
await restartWorkers();
|
|
1018
1095
|
break;
|
|
1019
1096
|
}
|
|
1097
|
+
case 'stop': {
|
|
1098
|
+
gracefulStop().catch((e) => {
|
|
1099
|
+
lCore.display('[master] [gracefulStop]', e);
|
|
1100
|
+
});
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1020
1103
|
case 'global': {
|
|
1021
1104
|
// --- 为所有子进程更新 global 变量 ---
|
|
1022
1105
|
for (const pid in workerList) {
|
|
@@ -56,6 +56,7 @@ export default class extends sCtr.Ctr {
|
|
|
56
56
|
coreUpdatecode(): Promise<string>;
|
|
57
57
|
coreReload(): Promise<string>;
|
|
58
58
|
coreRestart(): Promise<string>;
|
|
59
|
+
coreStop(): Promise<string>;
|
|
59
60
|
corePm2(): Promise<string>;
|
|
60
61
|
coreNpm(): Promise<any>;
|
|
61
62
|
coreGlobal(): Promise<string>;
|
package/www/example/ctr/test.js
CHANGED
|
@@ -169,6 +169,7 @@ export default class extends sCtr.Ctr {
|
|
|
169
169
|
`<br><a href="${this._config.const.urlBase}test/core-ls">View "test/core-ls"</a>`,
|
|
170
170
|
`<br><a href="${this._config.const.urlBase}test/core-reload">View "test/core-reload"</a>`,
|
|
171
171
|
`<br><a href="${this._config.const.urlBase}test/core-restart">View "test/core-restart"</a>`,
|
|
172
|
+
`<br><a href="${this._config.const.urlBase}test/core-stop">View "test/core-stop"</a>`,
|
|
172
173
|
`<br><a href="${this._config.const.urlBase}test/core-pm2?name=cron&action=restart">View "test/core-pm2"</a>`,
|
|
173
174
|
`<br><a href="${this._config.const.urlBase}test/core-npm">View "test/core-npm"</a>`,
|
|
174
175
|
`<br><a href="${this._config.const.urlBase}test/core-global">View "test/core-global"</a>`,
|
|
@@ -1572,6 +1573,10 @@ to: ${to}`
|
|
|
1572
1573
|
const list = await lCore.sendRestart();
|
|
1573
1574
|
return `The restart request has been sent, please review the console.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
|
|
1574
1575
|
}
|
|
1576
|
+
async coreStop() {
|
|
1577
|
+
const list = await lCore.sendStop();
|
|
1578
|
+
return `The stop request has been sent, please review the console.<br>Hosts: ${lText.stringifyJson(list)}<br><br>` + this._getEnd();
|
|
1579
|
+
}
|
|
1575
1580
|
async corePm2() {
|
|
1576
1581
|
const name = this._get['name'] ?? '';
|
|
1577
1582
|
const action = (this._get['action'] ?? 'restart');
|
|
@@ -2499,10 +2504,29 @@ error: <pre>${JSON.stringify(res.error, null, 4)}</pre>`);
|
|
|
2499
2504
|
const echo = [];
|
|
2500
2505
|
const json = await lUndici.getResponseJson(this._internalUrl + 'test/undici-get-response-json1');
|
|
2501
2506
|
echo.push(`<pre>const json = await lUndici.getResponseJson('${this._internalUrl}test/undici-get-response-json1');</pre>
|
|
2502
|
-
result: <pre>${lText.htmlescape(JSON.stringify(json, null, 4))}</pre>`);
|
|
2507
|
+
result: <pre>${lText.htmlescape(JSON.stringify(json, null, 4))}</pre><hr>`);
|
|
2503
2508
|
const json2 = await lUndici.getResponseJson(this._internalUrl + 'test/undici-get-response-json2');
|
|
2504
2509
|
echo.push(`<pre>const json2 = await lUndici.getResponseJson('${this._internalUrl}test/undici-get-response-json2');</pre>
|
|
2505
|
-
result: <pre>${lText.htmlescape(JSON.stringify(json2, null, 4))}</pre>`);
|
|
2510
|
+
result: <pre>${lText.htmlescape(JSON.stringify(json2, null, 4))}</pre><hr>`);
|
|
2511
|
+
/** --- JSON 重试验证方法的执行次数 --- */
|
|
2512
|
+
let retryJsonHandlerCount = 0;
|
|
2513
|
+
const json3 = await lUndici.getResponseJson(this._internalUrl + 'test/undici-get-response-json1', {
|
|
2514
|
+
'retryJson': 1,
|
|
2515
|
+
'retryJsonHandler': (value) => {
|
|
2516
|
+
++retryJsonHandlerCount;
|
|
2517
|
+
return (retryJsonHandlerCount > 1) && (value.result === 1);
|
|
2518
|
+
},
|
|
2519
|
+
});
|
|
2520
|
+
echo.push(`<pre>let retryJsonHandlerCount = 0;
|
|
2521
|
+
const json3 = await lUndici.getResponseJson('${this._internalUrl}test/undici-get-response-json1', {
|
|
2522
|
+
'retryJson': 1,
|
|
2523
|
+
'retryJsonHandler': (value) => {
|
|
2524
|
+
++retryJsonHandlerCount;
|
|
2525
|
+
return (retryJsonHandlerCount > 1) && (value.result === 1);
|
|
2526
|
+
},
|
|
2527
|
+
});</pre>
|
|
2528
|
+
handler count: ${retryJsonHandlerCount}
|
|
2529
|
+
result: <pre>${lText.htmlescape(JSON.stringify(json3, null, 4))}</pre>`);
|
|
2506
2530
|
return echo.join('') + '<br>' + this._getEnd();
|
|
2507
2531
|
}
|
|
2508
2532
|
undiciGetResponseJson1() {
|