multi-tasks 3.2.3 → 3.4.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.
- package/README.md +14 -12
- package/package.json +36 -36
- package/utils/makedir.js +12 -18
- package/utils/progressCtl.js +287 -118
- package/utils/progressView.js +110 -35
- package/utils/randoms.js +5 -2
- package/utils/startupBanner.js +2 -1
- package/utils/workerStatusProbe.js +38 -0
- package/workers/Multithread.js +83 -44
- package/workers/TaskMgr.js +406 -293
- package/workers/WorkerMgr.js +18 -27
- package/workers/asMaster.js +101 -100
package/utils/progressView.js
CHANGED
|
@@ -1,35 +1,110 @@
|
|
|
1
|
-
//终端进度条渲染器(零依赖):
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
//终端进度条渲染器(零依赖): N 行帧——首行 bar+百分比固定 36 列(约 18 个中文字符宽, 不随终端宽度变), 末行实时计数(动态宽, 封顶 cols-1),
|
|
2
|
+
//中间可接 extraLines 附加行(如 worker 状态行, 同样封顶 cols-1); 帧行数帧间必须恒定(由调用方保证), 是原地重绘的前提。
|
|
3
|
+
//bar 行短则拖动窗口时几乎不触发终端折行重排(reflow), ESC[nA 数行更可信(占满整行方案在 conpty 重排下
|
|
4
|
+
//反复错位, 用户拍板改短行)。首帧直接写整帧(无光标控制); 后续帧 \r 回行首 + 清行 + 逐行 ESC[1A 上移清行(N-1 次) + 重写整帧。
|
|
5
|
+
//resize 重锚定(reanchor)是抹除式: 终端重排(reflow)后旧帧占用行数不可信, 按记录的上帧
|
|
6
|
+
//各行可见长度 ÷ 当前列宽算出实际占用行数, 上移回旧帧首行 + ESC[0J 清到屏尾, 下一帧从锚点重写——
|
|
7
|
+
//不做换行定格: 定格会把重排碎片留在滚动区, 连续拖动即满屏碎片(用户否掉的旧方案)。
|
|
8
|
+
//已知取舍: 两帧之间若有外部写入共享终端, ESC[nA 数行会被挤偏, 旧帧可能滞留残留(用户确认接受)。
|
|
9
|
+
const ESC = '\x1B';
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const RED = '\x1B[31m';
|
|
12
|
+
const GREEN = '\x1B[32m';
|
|
13
|
+
const YELLOW = '\x1B[33m';
|
|
14
|
+
const RESET_FG = '\x1B[39m';
|
|
15
|
+
|
|
16
|
+
const BAR_OVERHEAD = 10;//'[' + '] ' + 6 位百分比 + '%'
|
|
17
|
+
const BAR_COLS = 36;//bar 行固定 36 列(约 18 个中文字符宽, 用户拍板, 不随终端宽度变): 行短则拖动窗口时几乎不触发折行重排; info 行保持动态宽(计数器全显)
|
|
18
|
+
|
|
19
|
+
//仅渲染帧上色(writeLine/run.log 永远无色): bar 完成段绿 / done 数绿 / succ 数绿 / running 数黄 / failed 整词红 / succ-but-save-failed 数黄,
|
|
20
|
+
//为 0 不着色减少噪音。必须在按列宽截断之后调用: 颜色码不占可见宽度, 也不会被截断拆开
|
|
21
|
+
const colorizeFrame = (line) => {
|
|
22
|
+
return line
|
|
23
|
+
.replace(/\[([█]+)/, (m, blocks) => `[${GREEN}${blocks}${RESET_FG}`)
|
|
24
|
+
.replace(/(\d+) failed/, (m, n) => (n === '0' ? m : `${RED}${n} failed${RESET_FG}`))
|
|
25
|
+
.replace(/(\d+) succ-but-save-failed/, (m, n) => (n === '0' ? m : `${YELLOW}${n}${RESET_FG} succ-but-save-failed`))
|
|
26
|
+
.replace(/(\d+) running/, (m, n) => (n === '0' ? m : `${YELLOW}${n}${RESET_FG} running`))
|
|
27
|
+
.replace(/(\d+) succ(?=,|$)/, (m, n) => (n === '0' ? m : `${GREEN}${n}${RESET_FG} succ`))//(?=,|$) 防误配 succ-but-save-failed 的词头
|
|
28
|
+
.replace(/(\d+)\/(\d+) done/, (m, done, total) => (done === '0' ? m : `${GREEN}${done}${RESET_FG}/${total} done`));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const create = ({ stream }) => {
|
|
32
|
+
if (!stream) { throw new Error('progressView needs a stream'); };
|
|
33
|
+
let frameCount = 0;
|
|
34
|
+
let ended = false;
|
|
35
|
+
let lastLineLens = [];//上帧各行可见长度(截断后、上色前): resize 重排后算旧帧实际占用行数用
|
|
36
|
+
|
|
37
|
+
//TTY 现场排查开关: MULTI_TASKS_PROGRESS_DEBUG=文件路径 时, 每帧字节流+当时列宽记成 JSON 行,
|
|
38
|
+
//供 test/terminalSim.js 回放比对真实终端行为(仅排查用, 平时为空不写盘)
|
|
39
|
+
const debugPath = process.env.MULTI_TASKS_PROGRESS_DEBUG;
|
|
40
|
+
const write = (s) => {
|
|
41
|
+
if (debugPath) {
|
|
42
|
+
fs.appendFileSync(debugPath, `${JSON.stringify({ at: Date.now(), cols: stream.columns || 80, data: s })}\n`);
|
|
43
|
+
};
|
|
44
|
+
stream.write(s);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const buildLines = ({ done, succ, running, remaining, failed, succButSaveFailed, extraLines }) => {
|
|
48
|
+
//行比终端列宽还长会折行、光标定位错乱: 每行封顶 cols-1(截断后才上色);
|
|
49
|
+
//留 1 空列: 写满整列会触发终端满列换行(deferred wrap), resize 重排时 ESC[1A 数行失准;
|
|
50
|
+
//bar 行固定 BAR_COLS 列(窄终端整行截断, 行尾百分比会被截掉): 行短则拖动窗口时几乎不触发折行重排
|
|
51
|
+
const cols = stream.columns || 80;
|
|
52
|
+
const maxLineLen = Math.max(1, cols - 1);
|
|
53
|
+
const truncate = (line) => (line.length > maxLineLen ? line.slice(0, maxLineLen) : line);
|
|
54
|
+
let total = done + running + remaining;
|
|
55
|
+
let width = BAR_COLS - BAR_OVERHEAD;//固定 26 格, 不随终端宽度变
|
|
56
|
+
let filled = total > 0 ? Math.floor(width * done / total) : width;
|
|
57
|
+
let bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
58
|
+
let pct = (total > 0 ? 100 * done / total : 100).toFixed(2).padStart(6);//两位小数, 定宽防行长抖动
|
|
59
|
+
//末行实时计数器(done/succ/running/failed/succ-but-save-failed), 与收尾计数器行同源(都出自 getFinalStats)
|
|
60
|
+
let barLine = `[${bar}] ${pct}%`;
|
|
61
|
+
let infoLine = `${done}/${total} done, ${succ || 0} succ, ${running} running, ${failed || 0} failed, ${succButSaveFailed || 0} succ-but-save-failed`;
|
|
62
|
+
//extraLines(状态行等)插在 bar 行与计数器行之间(计数器行固定帧尾——用户拍板):
|
|
63
|
+
//帧行数必须帧间恒定(ESC[nA 上移数按新帧行数算, 变行数会错位), 由调用方(progressCtl)保证开启期间每帧都带
|
|
64
|
+
let lines = [barLine];
|
|
65
|
+
if (extraLines) {
|
|
66
|
+
lines = lines.concat(extraLines);
|
|
67
|
+
};
|
|
68
|
+
lines.push(infoLine);
|
|
69
|
+
return lines.map(truncate);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const render = (data) => {
|
|
73
|
+
let lines = buildLines(data);
|
|
74
|
+
lastLineLens = lines.map((line) => line.length);
|
|
75
|
+
let frame = lines.map(colorizeFrame).join('\n');
|
|
76
|
+
if (frameCount === 0) {
|
|
77
|
+
write(frame);
|
|
78
|
+
} else {
|
|
79
|
+
//N 行帧同构推广: \r 回行首清当前行(帧末行) + 逐行上移清行(N-1 次) + 重写整帧
|
|
80
|
+
let prefix = `\r${ESC}[2K` + `${ESC}[1A${ESC}[2K`.repeat(lines.length - 1);
|
|
81
|
+
write(`${prefix}${frame}`);
|
|
82
|
+
};
|
|
83
|
+
frameCount++;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const end = () => {//收尾换行: 进度区定格, 后续输出(shell 提示符等)落在下一行
|
|
87
|
+
ended = true;
|
|
88
|
+
write('\n');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const reanchor = () => {//终端 resize 重排后旧帧行数不可信: 按上帧各行行长 ÷ 当前列宽算出实际占用行数,
|
|
92
|
+
//上移回旧帧首行并 ESC[0J 清到屏尾(抹除旧帧, 不留定格碎片), 下一帧从该锚点按首帧重写
|
|
93
|
+
if (ended || frameCount === 0) {
|
|
94
|
+
return;
|
|
95
|
+
};
|
|
96
|
+
const cols = stream.columns || 80;
|
|
97
|
+
const wrapRows = (len) => Math.max(1, Math.ceil(len / cols));
|
|
98
|
+
const rowsAbove = lastLineLens.reduce((sum, len) => sum + wrapRows(len), 0) - 1;
|
|
99
|
+
write(`\r${ESC}[${rowsAbove}A${ESC}[0J`);
|
|
100
|
+
frameCount = 0;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const writeLine = (text) => {//定格后整行输出(计数器行等): 纯文本一行, 不进 run.log(ASCII 契约之外的字符只走 render)
|
|
104
|
+
write(`${text}\n`);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return { render, end, reanchor, writeLine };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
module.exports = { create };
|
package/utils/randoms.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const fmtDigit = (n)=>{
|
|
3
4
|
return n > 9 ? "" + n: "0" + n;
|
|
4
5
|
}
|
|
@@ -13,9 +14,11 @@ const getDateTimeTxt = ()=>{
|
|
|
13
14
|
return `${YYYY}-${MM}-${DD}_${HH}-${mm}-${ss}`;
|
|
14
15
|
};
|
|
15
16
|
const get36Id = ()=>{
|
|
16
|
-
|
|
17
|
+
//时间戳前缀(保排序可读) + crypto 随机后缀(hex 是 base36 字符集子集):
|
|
18
|
+
//Math.random 拼数字会丢前导零且同毫秒时钟不变, 批量建任务熵不足(勿回退)
|
|
17
19
|
let num2 = Date.now().toString(36);
|
|
18
|
-
let
|
|
20
|
+
let rand = crypto.randomBytes(8).toString('hex');
|
|
21
|
+
let str = num2+rand;
|
|
19
22
|
return str;
|
|
20
23
|
};
|
|
21
24
|
module.exports = {getDateTimeTxt, get36Id};
|
package/utils/startupBanner.js
CHANGED
|
@@ -7,7 +7,7 @@ const runLog = require('./runLog');
|
|
|
7
7
|
|
|
8
8
|
const VERSION = require('../package.json').version;
|
|
9
9
|
|
|
10
|
-
const KEY_WIDTH =
|
|
10
|
+
const KEY_WIDTH = 19;//键列宽: 需容纳最长键 'monitor install cmd', 改动时同步调
|
|
11
11
|
|
|
12
12
|
const renderTitleBox = (mode) => {
|
|
13
13
|
const left = `multi-tasks v${VERSION}`;
|
|
@@ -27,6 +27,7 @@ const renderFieldLines = (info) => {
|
|
|
27
27
|
['new tasks', `${info.newTasks}`],
|
|
28
28
|
['run.log', info.sysLogFile || '(not available)'],
|
|
29
29
|
['started at', info.startedAt],
|
|
30
|
+
['monitor install cmd', 'npm install multi-tasks-monitor'],
|
|
30
31
|
['monitor start cmd', `npx multi-tasks-monitor --task-dir "${info.taskFolder}" --port 3777`],
|
|
31
32
|
];
|
|
32
33
|
return rows.map(([key, value]) => `${key.padEnd(KEY_WIDTH)} : ${value}`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//worker 端状态探针(零依赖, perf_hooks 为 Node 内置): unref 定时器按间隔自采——
|
|
2
|
+
//两个增量指标: ELU(eventLoopUtilization 快照差值的 utilization, 0~1, JS 线程打没打满的直接答案)与
|
|
3
|
+
//CPU%(process.cpuUsage 增量/墙钟增量, 单核占比, 用户开 worker_threads 时可 >100, 原样上报);
|
|
4
|
+
//两个内存快照: rss(进程物理内存)与 heapUsed(V8 堆), 原始字节上报, 格式化在 master。
|
|
5
|
+
//通过 send 回调发给 master。只在 worker 进程内运行; interval <= 0 不启动(特性关闭)。
|
|
6
|
+
//unref: 不阻碍进程退出, 不干扰 autoClose; 采样/上报异常只 runLog.warn, 绝不影响任务执行。
|
|
7
|
+
const { performance } = require('perf_hooks');
|
|
8
|
+
|
|
9
|
+
const runLog = require('./runLog');
|
|
10
|
+
|
|
11
|
+
const start = (intervalMs, send) => {
|
|
12
|
+
if (!(intervalMs > 0)) {
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
let lastElu = performance.eventLoopUtilization();
|
|
16
|
+
let lastCpu = process.cpuUsage();
|
|
17
|
+
const timer = setInterval(() => {
|
|
18
|
+
try {
|
|
19
|
+
const wallMs = intervalMs;//定时器间隔即墙钟窗口: fake timers/真实运行一致, 漂移纳入分子分母可忽略
|
|
20
|
+
const curElu = performance.eventLoopUtilization();
|
|
21
|
+
const elu = performance.eventLoopUtilization(curElu, lastElu).utilization;
|
|
22
|
+
const cpuDelta = process.cpuUsage(lastCpu);
|
|
23
|
+
const cpu = ((cpuDelta.user + cpuDelta.system) / (wallMs * 1000)) * 100;
|
|
24
|
+
const mem = process.memoryUsage();//~0.6µs/次, 每帧一次可忽略
|
|
25
|
+
lastElu = curElu;
|
|
26
|
+
lastCpu = process.cpuUsage();
|
|
27
|
+
send({ elu, cpu, rss: mem.rss, heapUsed: mem.heapUsed });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
runLog.warn('[Worker]: status probe failed, skip this frame', e);
|
|
30
|
+
};
|
|
31
|
+
}, intervalMs);
|
|
32
|
+
timer.unref();
|
|
33
|
+
return {
|
|
34
|
+
stop: () => { clearInterval(timer); },
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = { start };
|
package/workers/Multithread.js
CHANGED
|
@@ -12,22 +12,12 @@ const helper = require('./helper');
|
|
|
12
12
|
const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
|
|
13
13
|
const { resolveWorkerPriority } = require('../utils/resolvePriority');
|
|
14
14
|
const runLog = require('../utils/runLog');
|
|
15
|
+
const randomsUtil = require('../utils/randoms');
|
|
15
16
|
const progressCtl = require('../utils/progressCtl');
|
|
17
|
+
const workerStatusProbe = require('../utils/workerStatusProbe');
|
|
16
18
|
const startupBanner = require('../utils/startupBanner');
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
return n > 9 ? "" + n: "0" + n;
|
|
20
|
-
};
|
|
21
|
-
let getDateTimeTxt = ()=>{
|
|
22
|
-
let d=new Date();
|
|
23
|
-
let YYYY=d.getFullYear()
|
|
24
|
-
let MM=fmtdigit(d.getMonth()+1);
|
|
25
|
-
let DD=fmtdigit(d.getDate());
|
|
26
|
-
let HH=fmtdigit(d.getHours());
|
|
27
|
-
let mm=fmtdigit(d.getMinutes());
|
|
28
|
-
let ss=fmtdigit(d.getSeconds());
|
|
29
|
-
return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
|
|
30
|
-
};
|
|
20
|
+
//时间戳统一走 utils/randoms.getDateTimeTxt(YYYY-MM-DD_HH-mm-ss), 与 asMaster/user_messages 同口径
|
|
31
21
|
|
|
32
22
|
let USER_CONFIG;
|
|
33
23
|
|
|
@@ -75,25 +65,40 @@ const start = (config)=>{
|
|
|
75
65
|
delete workerTaskTimers[workerId];
|
|
76
66
|
}
|
|
77
67
|
};
|
|
68
|
+
let terminateAllAndExit = ()=>{
|
|
69
|
+
//shouldTerminate 与 TERMINATE_ALL_WORKERS 共用: 清在飞任务计时器 + 杀 worker + 进度条定格退出
|
|
70
|
+
for (const id in workerTaskTimers) {
|
|
71
|
+
clearTaskTimer(id);
|
|
72
|
+
};
|
|
73
|
+
killAllWorkers();
|
|
74
|
+
//计数器行与正常完成路径一致: 始终进 run.log(progressBar 未接管终端时 echo 上屏);
|
|
75
|
+
//runLog 写流与 stdout 都是异步写, 先写完再延迟 exit, 防计数器行被 process.exit 截断
|
|
76
|
+
let finalStats = TaskMgr.getFinalStats();
|
|
77
|
+
runLog.write(`[Tasks] succeeded=${finalStats.succeeded}, failed=${finalStats.failed}, succ-but-save-failed=${finalStats.succButSaveFailed || 0}`);
|
|
78
|
+
progressCtl.finish();
|
|
79
|
+
setTimeout(()=>{
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}, 10);
|
|
82
|
+
};
|
|
78
83
|
let finalizeTimeoutTask = (subTask, cost)=>{
|
|
79
84
|
let subid = subTask.subid;
|
|
80
85
|
try{
|
|
81
86
|
let resultType = TaskMgr.getSavedResultType(subid);
|
|
82
87
|
if(resultType === 'succ'){//超时边界: worker 已成功写盘, 视为已完成
|
|
83
|
-
TaskMgr.
|
|
88
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished');
|
|
84
89
|
}else if(resultType === 'error'){//超时边界: worker 已自行记错误
|
|
85
|
-
TaskMgr.
|
|
90
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished_with_errors');
|
|
86
91
|
}else{
|
|
87
92
|
if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
88
93
|
let retryCount = TaskMgr.incrementTaskRetry(subid);
|
|
89
94
|
if(retryCount <= config.maxTaskRetries){
|
|
90
95
|
runLog.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
91
|
-
TaskMgr.
|
|
96
|
+
TaskMgr.mvTaskSync(subid, 'running', 'new');
|
|
92
97
|
return;
|
|
93
98
|
};
|
|
94
99
|
};
|
|
95
100
|
TaskMgr.saveTaskResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
|
|
96
|
-
TaskMgr.
|
|
101
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished_with_errors');
|
|
97
102
|
}
|
|
98
103
|
}catch(e){
|
|
99
104
|
runLog.warn('[Master]: failed to finalize timeout task', subid, e);
|
|
@@ -116,11 +121,11 @@ const start = (config)=>{
|
|
|
116
121
|
let retryCount = TaskMgr.incrementTaskRetry(subid);
|
|
117
122
|
if(retryCount <= config.maxTaskRetries){
|
|
118
123
|
runLog.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
|
|
119
|
-
TaskMgr.
|
|
124
|
+
TaskMgr.mvTaskSync(subid, 'running', 'new');
|
|
120
125
|
}else{
|
|
121
126
|
runLog.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
|
|
122
127
|
TaskMgr.saveTaskResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
|
|
123
|
-
TaskMgr.
|
|
128
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished_with_errors');
|
|
124
129
|
};
|
|
125
130
|
}catch(e){
|
|
126
131
|
runLog.warn('[Master]: failed to handle crashed task', subid, e);
|
|
@@ -138,12 +143,14 @@ const start = (config)=>{
|
|
|
138
143
|
};
|
|
139
144
|
};
|
|
140
145
|
worker.on('message', function(message) {
|
|
146
|
+
if(message && message.__workerStatus){//worker 状态周期自报: master 自己消费进状态表, 不转发 setSysListener, 不触发派发
|
|
147
|
+
progressCtl.workerStatus(worker.id, message.__workerStatus);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
141
150
|
if(message && message.__sysEvent){//worker 系统事件: master 自己消费, 不转发
|
|
142
151
|
if(message.event === 'TERMINATE_ALL_WORKERS'){
|
|
143
152
|
runLog.warn(`[Master]: TERMINATE_ALL_WORKERS from worker ${worker.id}, killing all workers`);
|
|
144
|
-
|
|
145
|
-
progressCtl.finish();//单行进度条换行定格, 别让 shell 提示符盖住它
|
|
146
|
-
return process.exit(0);
|
|
153
|
+
return terminateAllAndExit();
|
|
147
154
|
}
|
|
148
155
|
let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
|
|
149
156
|
if(typeof USER_CONFIG.setSysListener === 'function'){
|
|
@@ -196,17 +203,23 @@ const start = (config)=>{
|
|
|
196
203
|
if(config.autoCloseAfterCompletion){
|
|
197
204
|
killWorker(worker);
|
|
198
205
|
}else{//waiting for new tasks
|
|
199
|
-
worker.send(MSG_NO_TASK_FOUND,()=>{}
|
|
200
|
-
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
201
|
-
});
|
|
206
|
+
worker.send(MSG_NO_TASK_FOUND,()=>{});
|
|
202
207
|
}
|
|
203
208
|
progressCtl.render();
|
|
204
209
|
return;
|
|
205
210
|
}
|
|
206
211
|
let terminateData = {taskCount,subTask,config, id: worker.id};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
let userWantsTerminate = false;
|
|
213
|
+
if(typeof USER_CONFIG.shouldTerminate === 'function'){
|
|
214
|
+
try{
|
|
215
|
+
userWantsTerminate = USER_CONFIG.shouldTerminate(terminateData)===true;
|
|
216
|
+
}catch(e){
|
|
217
|
+
//任务已 pop 就必须派出去: 抛错只记日志, 按「不终止」继续(裸 throw 会丢任务并死锁)
|
|
218
|
+
runLog.error('[Master]: shouldTerminate threw, continue dispatching task:', e);
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
if(userWantsTerminate) {
|
|
222
|
+
return terminateAllAndExit();
|
|
210
223
|
}
|
|
211
224
|
//send new tasks to the worker
|
|
212
225
|
let startTimestamp = new Date()*1;
|
|
@@ -219,9 +232,7 @@ const start = (config)=>{
|
|
|
219
232
|
startTimestamp,
|
|
220
233
|
config: ipcConfig,
|
|
221
234
|
subTask
|
|
222
|
-
},()=>{}
|
|
223
|
-
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
224
|
-
});
|
|
235
|
+
},()=>{});
|
|
225
236
|
progressCtl.taskAssigned(worker.id, subTask.subid);
|
|
226
237
|
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
227
238
|
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
@@ -252,7 +263,7 @@ const start = (config)=>{
|
|
|
252
263
|
pid: process.pid,
|
|
253
264
|
newTasks: TaskMgr.getTasks('new').length,
|
|
254
265
|
sysLogFile: config.__sysLogFile,
|
|
255
|
-
startedAt: getDateTimeTxt(),
|
|
266
|
+
startedAt: randomsUtil.getDateTimeTxt(),
|
|
256
267
|
};
|
|
257
268
|
console.log(startupBanner.renderBanner(startupInfo));
|
|
258
269
|
progressCtl.setup({//TTY 判定 + 渲染器创建都在控制器内; 统计口径注入, 控制器不依赖 TaskMgr
|
|
@@ -281,6 +292,7 @@ const start = (config)=>{
|
|
|
281
292
|
let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
|
|
282
293
|
clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
|
|
283
294
|
progressCtl.workerExited(worker.id);
|
|
295
|
+
WorkerMgr.removeWorker(worker.id);//清注册表引用: 崩溃补员场景 map 只增不减会累积死 worker
|
|
284
296
|
if (code !== 0 && !worker.exitedAfterDisconnect) {
|
|
285
297
|
runLog.write('Worker died unexpectedly starting a new one...');
|
|
286
298
|
workerCrashCount++;
|
|
@@ -311,18 +323,24 @@ const start = (config)=>{
|
|
|
311
323
|
succeeded: stats.succeeded,
|
|
312
324
|
failed: stats.failed,
|
|
313
325
|
failedTasks: stats.failedTasks,
|
|
326
|
+
succButSaveFailed: stats.succButSaveFailed || 0,//任务成功但结果保存失败(序列化兜底)的任务数, 明细见 .sys/user_messages
|
|
314
327
|
workers: {count: numOfWorkers, crashed: workerCrashCount},
|
|
315
328
|
};
|
|
316
329
|
let onFinishHelper = {
|
|
317
330
|
foreachSuccResult: (cb) => TaskMgr.forEachSuccResult(cb),
|
|
318
331
|
foreachErrorResult: (cb) => TaskMgr.forEachErrorResult(cb),
|
|
332
|
+
foreachResult: (cb) => TaskMgr.forEachResult(cb),
|
|
319
333
|
};
|
|
320
334
|
USER_CONFIG.onFinish(report, onFinishHelper);
|
|
321
335
|
delete USER_CONFIG.onFinish;
|
|
322
336
|
}
|
|
337
|
+
//计数器行先进 run.log(progressBar 未接管终端时 echo 上屏, 接管时由 progressCtl.finish() 打在屏幕);
|
|
338
|
+
//stdout/run.log 都是异步写, 写完留 10ms 再 exit, 防 process.exit 截断未 flush 的输出
|
|
339
|
+
let finalStats = TaskMgr.getFinalStats();
|
|
340
|
+
runLog.write(`[Tasks] succeeded=${finalStats.succeeded}, failed=${finalStats.failed}, succ-but-save-failed=${finalStats.succButSaveFailed || 0}`);
|
|
341
|
+
runLog.write('[Master]: all workers are done, process exit.');
|
|
342
|
+
progressCtl.finish();//单行进度条换行定格 + 计数器行, 别让 shell 提示符盖住它
|
|
323
343
|
setTimeout(()=>{
|
|
324
|
-
runLog.write('[Master]: all workers are done, process exit.')
|
|
325
|
-
progressCtl.finish();//单行进度条换行定格, 别让 shell 提示符盖住它
|
|
326
344
|
process.exit(0);
|
|
327
345
|
}, 10);
|
|
328
346
|
}
|
|
@@ -338,6 +356,16 @@ function subWorker(config){
|
|
|
338
356
|
//共享 stdout 为 TTY 时只写文件(与 master 判定一致), 否则 echo 同时上屏。判定收敛在 initAndHook 内,
|
|
339
357
|
//无路径时维持 console。入口即生效, 领不到任务的富余 worker 的 waiting 日志同样进 run.log。
|
|
340
358
|
runLog.initAndHook(config.__sysLogFile, { echo: !(config.progressBar !== false && process.stdout.isTTY) });
|
|
359
|
+
//状态探针: 启动条件与上面 echo 判定同口径(progressBar 开 + TTY), 另需 interval > 0(缺省 2000);
|
|
360
|
+
//非 TTY/关闭时不启动, 零开销零 IPC 噪音。探针 unref, 进程退出随进程销毁, 无需 stop
|
|
361
|
+
//布尔口径: false 等价 <=0 关闭, true 等价缺省(勿直接进 setInterval, true 会被当 1ms)
|
|
362
|
+
let statusInterval = config.worker_status_poll_ms;
|
|
363
|
+
statusInterval = (typeof statusInterval === 'undefined' || statusInterval === true) ? 2000 : (statusInterval === false ? 0 : statusInterval);
|
|
364
|
+
if(config.progressBar !== false && process.stdout.isTTY && statusInterval > 0){
|
|
365
|
+
workerStatusProbe.start(statusInterval, (payload)=>{
|
|
366
|
+
worker.send({__workerStatus: payload});
|
|
367
|
+
});
|
|
368
|
+
};
|
|
341
369
|
worker.on('message', function(message) {
|
|
342
370
|
if(message && message.__broadcast){//master 中转来的广播: 分发给 helper.on 注册的 handler
|
|
343
371
|
helper.__dispatchBroadcast(message);
|
|
@@ -358,7 +386,7 @@ function subWorker(config){
|
|
|
358
386
|
if(typeof subTask.taskCount === 'undefined') subTask.taskCount = taskCount;
|
|
359
387
|
subTask.workerId = worker.id;
|
|
360
388
|
subTask.workerPid = process.pid;
|
|
361
|
-
runLog.write(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, getDateTimeTxt());
|
|
389
|
+
runLog.write(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, randomsUtil.getDateTimeTxt());
|
|
362
390
|
let subTaskLog = {
|
|
363
391
|
workerId: worker.id,
|
|
364
392
|
workerPid: process.pid,
|
|
@@ -369,7 +397,7 @@ function subWorker(config){
|
|
|
369
397
|
if(tasklog.retried) return;//重试任务已回 new 等重跑, 未完成, 不打 [Task Finished]
|
|
370
398
|
let {cost, startTimestamp, endTimestamp} = tasklog;
|
|
371
399
|
|
|
372
|
-
runLog.write(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, getDateTimeTxt(), `cost=${cost}ms`)
|
|
400
|
+
runLog.write(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, randomsUtil.getDateTimeTxt(), `cost=${cost}ms`)
|
|
373
401
|
}).then(()=>{
|
|
374
402
|
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
375
403
|
});
|
|
@@ -381,17 +409,27 @@ function subWorker(config){
|
|
|
381
409
|
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
382
410
|
|
|
383
411
|
};
|
|
412
|
+
//错误对象 → 可 JSON 序列化的持久化形态: Error 取 message/stack, 其余原样;
|
|
413
|
+
//错误值不可 JSON 序列化(循环引用/BigInt 等)时换成机器可读占位, 保住「用户 reject 了什么」的语义
|
|
414
|
+
function serializeError(e){
|
|
415
|
+
let serialized;
|
|
416
|
+
if(e instanceof Error){
|
|
417
|
+
serialized = {message: e.message, stack: e.stack};
|
|
418
|
+
}else{
|
|
419
|
+
serialized = e;
|
|
420
|
+
};
|
|
421
|
+
try{
|
|
422
|
+
JSON.stringify(serialized);
|
|
423
|
+
}catch(stringifyErr){
|
|
424
|
+
serialized = {type: 'unserializable_error_value', message: 'error value is not JSON-serializable'};
|
|
425
|
+
};
|
|
426
|
+
return serialized;
|
|
427
|
+
};
|
|
384
428
|
function runTask(config, subTask, subTaskLog){
|
|
385
|
-
return new Promise((resolve)=>{
|
|
429
|
+
return new Promise((resolve)=>{
|
|
386
430
|
let resultPromise = Promise.resolve({});
|
|
387
431
|
let resultData;
|
|
388
432
|
let errors = [];
|
|
389
|
-
let serializeError = (e)=>{
|
|
390
|
-
if(e instanceof Error){
|
|
391
|
-
return {message: e.message, stack: e.stack};
|
|
392
|
-
}
|
|
393
|
-
return e;
|
|
394
|
-
};
|
|
395
433
|
helper.load(config);
|
|
396
434
|
TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
|
|
397
435
|
try{
|
|
@@ -429,6 +467,7 @@ function runTask(config, subTask, subTaskLog){
|
|
|
429
467
|
}catch(newPath){
|
|
430
468
|
runLog.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
|
|
431
469
|
};
|
|
470
|
+
TaskMgr.deleteUserMessage(subTask.subid);//重跑即新一轮: 清掉该任务前序尝试的遗留消息
|
|
432
471
|
};
|
|
433
472
|
};
|
|
434
473
|
TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
|
|
@@ -452,7 +491,7 @@ function runTask(config, subTask, subTaskLog){
|
|
|
452
491
|
cost,
|
|
453
492
|
retried: false
|
|
454
493
|
});
|
|
455
|
-
});
|
|
494
|
+
});
|
|
456
495
|
});
|
|
457
496
|
});
|
|
458
497
|
};
|