multi-tasks 3.4.1 → 3.5.1
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 +326 -277
- package/bench-storage.svg +17 -0
- package/package.json +3 -2
- package/utils/startupBanner.js +51 -51
- package/workers/IpcTaskStore.js +136 -0
- package/workers/MemoryStore.js +247 -0
- package/workers/Multithread.js +203 -49
- package/workers/TaskMgr.js +406 -406
- package/workers/asMaster.js +109 -101
- package/workers/helper.js +77 -67
- package/workers/index.js +28 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="252" viewBox="0 0 720 252" font-family="Segoe UI, Helvetica, Arial, sans-serif">
|
|
2
|
+
<rect x="1" y="1" width="718" height="250" rx="8" fill="#ffffff" stroke="#dadce0"/>
|
|
3
|
+
<text x="24" y="38" font-size="17" font-weight="600" fill="#202124">task_storage benchmark — 30,000 tasks × 28 workers</text>
|
|
4
|
+
<text x="24" y="60" font-size="12" fill="#5f6368">wall-clock time of a full run, average of 3 runs · lower is better</text>
|
|
5
|
+
|
|
6
|
+
<text x="112" y="114" font-size="14" fill="#202124" text-anchor="end">file</text>
|
|
7
|
+
<rect x="120" y="94" width="420" height="34" rx="3" fill="#9aa0a6"/>
|
|
8
|
+
<text x="548" y="116" font-size="14" fill="#202124">195.1s · 154 tasks/s</text>
|
|
9
|
+
|
|
10
|
+
<text x="112" y="172" font-size="14" fill="#202124" text-anchor="end">memory</text>
|
|
11
|
+
<rect x="120" y="152" width="24" height="34" rx="3" fill="#188038"/>
|
|
12
|
+
<text x="152" y="174" font-size="14" fill="#202124">11.2s · 2,676 tasks/s</text>
|
|
13
|
+
<text x="360" y="174" font-size="15" font-weight="700" fill="#188038">17.4× faster</text>
|
|
14
|
+
|
|
15
|
+
<line x1="120" y1="208" x2="540" y2="208" stroke="#e8eaed"/>
|
|
16
|
+
<text x="24" y="232" font-size="11" fill="#5f6368">per-round walls — file: 202.7 / 193.6 / 188.9s · memory: 11.3 / 11.1 / 11.3s · Windows 11, SSD · script: test-manual/bench-storage.js</text>
|
|
17
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multi-tasks",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "Multi-process task scheduling based on Node.js cluster, with crash resume and failed-task restart support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.js",
|
|
8
8
|
"workers",
|
|
9
|
-
"utils/*.js"
|
|
9
|
+
"utils/*.js",
|
|
10
|
+
"bench-storage.svg"
|
|
10
11
|
],
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
package/utils/startupBanner.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
//启动信息横幅渲染器(纯函数, 不碰 console/fs/cluster): 仅标题行 ASCII 框装饰,
|
|
2
|
-
//框宽只由标题行内容决定——长路径只出现在平铺字段行, 不会撑破框、不会折行。
|
|
3
|
-
//renderBanner: 终端上屏用(劫持前 console 直出); renderLogHeader: run.log 文件头部用(含 MODE 行);
|
|
4
|
-
//writeLogHeader: 判 sysLogFile 后写 runLog——判断收敛在方法内部, 调用点不写 if;
|
|
5
|
-
//无路径时(内部 API multiTasks.start 自动生成目录名)no-op, 不透传 console。
|
|
6
|
-
const runLog = require('./runLog');
|
|
7
|
-
|
|
8
|
-
const VERSION = require('../package.json').version;
|
|
9
|
-
|
|
10
|
-
const KEY_WIDTH = 19;//键列宽: 需容纳最长键 'monitor install cmd', 改动时同步调
|
|
11
|
-
|
|
12
|
-
const renderTitleBox = (mode) => {
|
|
13
|
-
const left = `multi-tasks v${VERSION}`;
|
|
14
|
-
const right = `MODE: ${mode}`;
|
|
15
|
-
const innerWidth = left.length + right.length + 4;//左右内容间至少 4 空格
|
|
16
|
-
const middle = `| ${left}${' '.repeat(innerWidth - left.length - right.length)}${right} |`;
|
|
17
|
-
const border = `+${'-'.repeat(innerWidth + 2)}+`;
|
|
18
|
-
return [border, middle, border];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const renderFieldLines = (info) => {
|
|
22
|
-
const rows = [
|
|
23
|
-
['taskId', info.taskId],
|
|
24
|
-
['taskFolder', info.taskFolder],
|
|
25
|
-
['workers', `${info.numOfWorkers} (numCPUs=${info.numCPUs})`],
|
|
26
|
-
['master PID', `${info.pid}`],
|
|
27
|
-
['new tasks', `${info.newTasks}`],
|
|
28
|
-
['run.log', info.sysLogFile || '(not available)'],
|
|
29
|
-
['started at', info.startedAt],
|
|
30
|
-
['monitor install cmd', 'npm install multi-tasks-monitor'],
|
|
31
|
-
['monitor start cmd', `npx multi-tasks-monitor --task-dir "${info.taskFolder}" --port 3777`],
|
|
32
|
-
];
|
|
33
|
-
return rows.map(([key, value]) => `${key.padEnd(KEY_WIDTH)} : ${value}`);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const renderBanner = (info) => {
|
|
37
|
-
return [...renderTitleBox(info.mode), ...renderFieldLines(info)].join('\n');
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const renderLogHeader = (info) => {
|
|
41
|
-
return [`${'MODE'.padEnd(KEY_WIDTH)} : ${info.mode}`, ...renderFieldLines(info)].join('\n');
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const writeLogHeader = (info) => {
|
|
45
|
-
if (!info.sysLogFile) {
|
|
46
|
-
return;
|
|
47
|
-
};
|
|
48
|
-
runLog.write(renderLogHeader(info));
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
module.exports = { renderBanner, renderLogHeader, writeLogHeader };
|
|
1
|
+
//启动信息横幅渲染器(纯函数, 不碰 console/fs/cluster): 仅标题行 ASCII 框装饰,
|
|
2
|
+
//框宽只由标题行内容决定——长路径只出现在平铺字段行, 不会撑破框、不会折行。
|
|
3
|
+
//renderBanner: 终端上屏用(劫持前 console 直出); renderLogHeader: run.log 文件头部用(含 MODE 行);
|
|
4
|
+
//writeLogHeader: 判 sysLogFile 后写 runLog——判断收敛在方法内部, 调用点不写 if;
|
|
5
|
+
//无路径时(内部 API multiTasks.start 自动生成目录名)no-op, 不透传 console。
|
|
6
|
+
const runLog = require('./runLog');
|
|
7
|
+
|
|
8
|
+
const VERSION = require('../package.json').version;
|
|
9
|
+
|
|
10
|
+
const KEY_WIDTH = 19;//键列宽: 需容纳最长键 'monitor install cmd', 改动时同步调
|
|
11
|
+
|
|
12
|
+
const renderTitleBox = (mode) => {
|
|
13
|
+
const left = `multi-tasks v${VERSION}`;
|
|
14
|
+
const right = `MODE: ${mode}`;
|
|
15
|
+
const innerWidth = left.length + right.length + 4;//左右内容间至少 4 空格
|
|
16
|
+
const middle = `| ${left}${' '.repeat(innerWidth - left.length - right.length)}${right} |`;
|
|
17
|
+
const border = `+${'-'.repeat(innerWidth + 2)}+`;
|
|
18
|
+
return [border, middle, border];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const renderFieldLines = (info) => {
|
|
22
|
+
const rows = [
|
|
23
|
+
['taskId', info.taskId],
|
|
24
|
+
['taskFolder', info.taskFolder],
|
|
25
|
+
['workers', `${info.numOfWorkers} (numCPUs=${info.numCPUs})`],
|
|
26
|
+
['master PID', `${info.pid}`],
|
|
27
|
+
['new tasks', `${info.newTasks}`],
|
|
28
|
+
['run.log', info.sysLogFile || '(not available)'],
|
|
29
|
+
['started at', info.startedAt],
|
|
30
|
+
['monitor install cmd', 'npm install multi-tasks-monitor'],
|
|
31
|
+
['monitor start cmd', `npx multi-tasks-monitor --task-dir "${info.taskFolder}" --port 3777`],
|
|
32
|
+
];
|
|
33
|
+
return rows.map(([key, value]) => `${key.padEnd(KEY_WIDTH)} : ${value}`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const renderBanner = (info) => {
|
|
37
|
+
return [...renderTitleBox(info.mode), ...renderFieldLines(info)].join('\n');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const renderLogHeader = (info) => {
|
|
41
|
+
return [`${'MODE'.padEnd(KEY_WIDTH)} : ${info.mode}`, ...renderFieldLines(info)].join('\n');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const writeLogHeader = (info) => {
|
|
45
|
+
if (!info.sysLogFile) {
|
|
46
|
+
return;
|
|
47
|
+
};
|
|
48
|
+
runLog.write(renderLogHeader(info));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = { renderBanner, renderLogHeader, writeLogHeader };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const randomsUtil = require('../utils/randoms');
|
|
2
|
+
const runLog = require('../utils/runLog');
|
|
3
|
+
|
|
4
|
+
//memory 档 worker 侧存储后端: 把 TaskMgr 的目录操作逐条翻译成 {__storageOp} IPC 消息发 master,
|
|
5
|
+
//与 file 档盘操作逐条同构(禁止捎带/合并消息; 交卷与领卷是两个动作, 严格先交后领)。
|
|
6
|
+
//无返回值操作 fire-and-forget(等价 file 档同步写盘「调用返回即生效」, 同 worker IPC 通道 FIFO 保序);
|
|
7
|
+
//需返回值的操作(incrementTaskRetry/mvTask/mvTaskPromise)带 needAck, 等 master 的 {__storageOpResult} 回执。
|
|
8
|
+
//回执匹配无 opId: master 串行应用按序回执, pending 队列按序 shift 即可对齐。
|
|
9
|
+
//__storageOp/__storageOpResult 是库占用消息字段, 与 __broadcast/__sysEvent/__workerStatus 同级, 勿复用。
|
|
10
|
+
const UNDEFINED_SENTINEL = '__multitasks_undefined__';//JSON 数组里 undefined 会变 null, 用哨兵对象保住语义(file 档 undefined 结果写字面量 'undefined')
|
|
11
|
+
|
|
12
|
+
let masterTaskId = null;
|
|
13
|
+
let pendingAcks = [];//needAck 回执等待队列, 按发送顺序 shift
|
|
14
|
+
|
|
15
|
+
const encodeArg = (value)=>{
|
|
16
|
+
if(typeof value === 'undefined') return {[UNDEFINED_SENTINEL]: true};
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const send = (op, args, needAck)=>{
|
|
21
|
+
let message = {__storageOp: true, op, args: args.map(encodeArg)};
|
|
22
|
+
if(!needAck){
|
|
23
|
+
process.send(message);
|
|
24
|
+
return undefined;
|
|
25
|
+
};
|
|
26
|
+
message.needAck = true;
|
|
27
|
+
return new Promise((resolve, reject)=>{
|
|
28
|
+
process.send(message);//先送后入队: 回执最快也要下个事件循环才到, 不会错位
|
|
29
|
+
pendingAcks.push({resolve, reject});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const load = (config)=>{
|
|
34
|
+
masterTaskId = config.masterTaskId;
|
|
35
|
+
return IpcTaskStore;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const saveTaskLog = (subid, log)=>{
|
|
39
|
+
send('saveLog', [subid, log]);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const saveUserMessage = (message)=>{
|
|
43
|
+
send('saveUserMessage', [message]);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const saveTaskResult = (subid, hasError, data)=>{
|
|
47
|
+
//序列化检测必须在 worker 侧(IPC 走 JSON, 不可序列化的结果到不了 master):
|
|
48
|
+
//占位对象 + warn 用户消息, 语义同 TaskMgr.saveTaskResult 兜底(不翻转任务成败)
|
|
49
|
+
if(typeof data !== 'undefined'){
|
|
50
|
+
try{
|
|
51
|
+
let json = JSON.stringify(data);
|
|
52
|
+
if(typeof json !== 'string'){
|
|
53
|
+
throw new Error('result value is not JSON-serializable (function or symbol)');
|
|
54
|
+
};
|
|
55
|
+
}catch(serializeErr){
|
|
56
|
+
runLog.error('[Task Error] failed to serialize task result', subid, serializeErr);//runLog 留证与 file 档(TaskMgr.saveTaskResult)同口径
|
|
57
|
+
let placeholder = {
|
|
58
|
+
type: 'result_serialize_error',
|
|
59
|
+
message: serializeErr && serializeErr.message ? serializeErr.message : String(serializeErr),
|
|
60
|
+
};
|
|
61
|
+
data = hasError ? [placeholder] : placeholder;
|
|
62
|
+
saveUserMessage({
|
|
63
|
+
type: 'result_serialize_error',
|
|
64
|
+
level: 'warn',
|
|
65
|
+
subid,
|
|
66
|
+
side: hasError ? 'error' : 'result',
|
|
67
|
+
message: placeholder.message,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
send('saveResult', [subid, hasError, data]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const deleteSavedResult = (subid)=>{
|
|
75
|
+
send('deleteResult', [subid]);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const deleteUserMessage = (subid)=>{
|
|
79
|
+
send('deleteUserMessage', [subid]);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
//动态任务即时生效(禁止攒批): 本地按 TaskMgr 同规则赋 subid 后立即发 createTask 消息
|
|
83
|
+
const createNewTask = (task)=>{
|
|
84
|
+
task.masterTaskId = masterTaskId;
|
|
85
|
+
task.subid = `${masterTaskId}--${randomsUtil.get36Id()}`;
|
|
86
|
+
send('createTask', [task]);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const mvTask = (subid, from, to, callback)=>{
|
|
90
|
+
send('mvTask', [subid, from, to], true).then((value)=>{
|
|
91
|
+
callback(value.newPath, value.succ);
|
|
92
|
+
}).catch((e)=>{
|
|
93
|
+
runLog.warn('[IpcTaskStore]: mvTask ack failed', subid, e);
|
|
94
|
+
callback(null, false);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const mvTaskPromise = (subid, from, to)=>{
|
|
99
|
+
return send('mvTask', [subid, from, to], true).then((value)=>{
|
|
100
|
+
if(value.succ) return value.newPath;
|
|
101
|
+
throw value.newPath;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const incrementTaskRetry = (subid)=>{
|
|
106
|
+
return send('incrementRetry', [subid], true);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//内部方法: subWorker 消息链收到 __storageOpResult 时调用, FIFO shift 队首兑付
|
|
110
|
+
const __dispatchStorageOpResult = (message)=>{
|
|
111
|
+
let pending = pendingAcks.shift();
|
|
112
|
+
if(!pending) return;//无等待者的回执(乱序/重复): 丢弃
|
|
113
|
+
if(message.ok){
|
|
114
|
+
pending.resolve(message.value);
|
|
115
|
+
}else{
|
|
116
|
+
let errMsg = (message.error && message.error.message) ? message.error.message : 'storage op failed';
|
|
117
|
+
pending.reject(new Error(errMsg));
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const IpcTaskStore = {
|
|
122
|
+
load,
|
|
123
|
+
saveTaskLog,
|
|
124
|
+
saveTaskResult,
|
|
125
|
+
deleteSavedResult,
|
|
126
|
+
saveUserMessage,
|
|
127
|
+
deleteUserMessage,
|
|
128
|
+
createNewTask,
|
|
129
|
+
mvTask,
|
|
130
|
+
mvTaskPromise,
|
|
131
|
+
incrementTaskRetry,
|
|
132
|
+
__dispatchStorageOpResult,
|
|
133
|
+
__UNDEFINED_SENTINEL: UNDEFINED_SENTINEL,//master 解码 args 用(单一来源, 勿复制字面量)
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
module.exports = IpcTaskStore;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const randomsUtil = require('../utils/randoms');
|
|
2
|
+
const runLog = require('../utils/runLog');
|
|
3
|
+
|
|
4
|
+
//MemoryStore: memory 档 master 侧的内存任务存储, file 档磁盘三态目录(TaskMgr)的内存等价物。
|
|
5
|
+
// 任务四态(new/running/finished/finished_with_errors)、结果、任务日志、user messages 全部在内存,
|
|
6
|
+
// 操作粒度与 TaskMgr 对齐, 供 Multithread 派发复用。
|
|
7
|
+
const VALID_STATES = ['new', 'running', 'finished', 'finished_with_errors'];
|
|
8
|
+
const TERMINAL_STATES = ['finished', 'finished_with_errors'];
|
|
9
|
+
|
|
10
|
+
const createMemoryStore = (config) => {
|
|
11
|
+
const { masterTaskId, taskFolder } = config || {};
|
|
12
|
+
if (!masterTaskId || !taskFolder) {
|
|
13
|
+
throw new Error('createMemoryStore: config requires {masterTaskId, taskFolder}');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const records = new Map();//subid -> {state, task}
|
|
17
|
+
const newQueue = [];//new 态任务对象 FIFO 队列(getTasks('new') 返回本体)
|
|
18
|
+
const finishedOrder = [];//subid 首次进入终态的顺序, forEach 按它遍历
|
|
19
|
+
const results = new Map();//subid -> {hasError, data}
|
|
20
|
+
const logs = new Map();//subid -> log
|
|
21
|
+
const messages = new Map();//文件名 `<type>.<subid>.json` -> message
|
|
22
|
+
const messageSubids = new Set();//有消息的去重 subid 集合, stats().succButSaveFailed 口径(对齐 TaskMgr 一任务多消息只计一次)
|
|
23
|
+
|
|
24
|
+
const assertState = (state) => {
|
|
25
|
+
if (!VALID_STATES.includes(state)) {
|
|
26
|
+
throw new Error(`MemoryStore: invalid task state "${state}"`);
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//消息文件名规范同 TaskMgr 头部约定: 文件名 = <type>.<subid>.json, type 只允许 [a-z0-9_](不含点)
|
|
31
|
+
const MESSAGE_TYPE_PATTERN = /^[a-z0-9_]+$/;
|
|
32
|
+
|
|
33
|
+
const messageFname = (message) => `${message.type}.${message.subid}.json`;
|
|
34
|
+
|
|
35
|
+
const add = (task) => {
|
|
36
|
+
if (!task.subid) {
|
|
37
|
+
task.masterTaskId = masterTaskId;
|
|
38
|
+
task.subid = `${masterTaskId}--${randomsUtil.get36Id()}`;
|
|
39
|
+
};
|
|
40
|
+
//同 subid 重复 add 按 file 档「覆盖同名文件」语义处理: records 覆盖;
|
|
41
|
+
//旧项若还在 new 队列先剔除再入队尾, 防同 subid 双派
|
|
42
|
+
const existing = records.get(task.subid);
|
|
43
|
+
if (existing && existing.state === 'new') {
|
|
44
|
+
const index = newQueue.findIndex((item) => item.subid === task.subid);
|
|
45
|
+
if (index >= 0) {
|
|
46
|
+
newQueue.splice(index, 1);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
records.set(task.subid, { state: 'new', task });
|
|
50
|
+
newQueue.push(task);
|
|
51
|
+
return task;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const addMany = (tasks) => {
|
|
55
|
+
tasks.forEach(add);
|
|
56
|
+
return tasks;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const pop = () => {
|
|
60
|
+
const task = newQueue.shift();
|
|
61
|
+
if (!task) {
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
records.get(task.subid).state = 'running';
|
|
65
|
+
return task;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const saveLog = (subid, log) => {
|
|
69
|
+
logs.set(subid, log);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const saveResult = (subid, hasError, data) => {
|
|
73
|
+
results.set(subid, { hasError, data });
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const deleteResult = (subid) => {
|
|
77
|
+
results.delete(subid);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const move = (subid, from, to) => {
|
|
81
|
+
assertState(from);
|
|
82
|
+
assertState(to);
|
|
83
|
+
const rec = records.get(subid);
|
|
84
|
+
if (!rec || rec.state !== from) {
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
if (from === 'new') {
|
|
88
|
+
const index = newQueue.findIndex((task) => task.subid === subid);
|
|
89
|
+
if (index >= 0) {
|
|
90
|
+
newQueue.splice(index, 1);
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
rec.state = to;
|
|
94
|
+
if (to === 'new') {
|
|
95
|
+
newQueue.push(rec.task);
|
|
96
|
+
};
|
|
97
|
+
if (TERMINAL_STATES.includes(to) && !finishedOrder.includes(subid)) {
|
|
98
|
+
finishedOrder.push(subid);
|
|
99
|
+
};
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//对齐 file 档: TaskMgr.incrementTaskRetry 读 running/ 目录里的任务文件, 任务不在 running 即 ENOENT
|
|
104
|
+
//抛错不增计数——此处同样只在 running 态增计数; 崩溃/超时兜底与 worker 重试竞争时(任务已被对方移回
|
|
105
|
+
// new)不得重复增计数, 否则配额被虚耗、且超限分支会给仍在排队的任务种下 stale worker_crash 结果
|
|
106
|
+
const incrementRetry = (subid) => {
|
|
107
|
+
const rec = records.get(subid);
|
|
108
|
+
if (!rec) {
|
|
109
|
+
return 0;
|
|
110
|
+
};
|
|
111
|
+
if (rec.state !== 'running') {
|
|
112
|
+
return rec.task.__retryCount || 0;
|
|
113
|
+
};
|
|
114
|
+
rec.task.__retryCount = (rec.task.__retryCount || 0) + 1;
|
|
115
|
+
return rec.task.__retryCount;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const addMessage = (message) => {
|
|
119
|
+
//对齐 TaskMgr.saveUserMessage 口径: type 不合规拒写(只记日志), level 缺省 info, 补 timestamp
|
|
120
|
+
if (!MESSAGE_TYPE_PATTERN.test(message.type)) {
|
|
121
|
+
runLog.error('[Task Error] invalid user message type, skipped', message.type, message.subid);
|
|
122
|
+
return;
|
|
123
|
+
};
|
|
124
|
+
const enriched = { level: 'info', ...message, timestamp: randomsUtil.getDateTimeTxt() };
|
|
125
|
+
messages.set(messageFname(enriched), enriched);
|
|
126
|
+
messageSubids.add(enriched.subid);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clearMessages = (subid) => {
|
|
130
|
+
messages.forEach((message, fname) => {
|
|
131
|
+
if (message.subid === subid) {
|
|
132
|
+
messages.delete(fname);
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
messageSubids.delete(subid);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const getSavedResultType = (subid) => {
|
|
139
|
+
const entry = results.get(subid);
|
|
140
|
+
if (!entry) {
|
|
141
|
+
return null;
|
|
142
|
+
};
|
|
143
|
+
return entry.hasError ? 'error' : 'succ';
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getState = (subid) => {
|
|
147
|
+
const rec = records.get(subid);
|
|
148
|
+
return rec ? rec.state : null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const getTasks = (state) => {
|
|
152
|
+
assertState(state);
|
|
153
|
+
if (state === 'new') {
|
|
154
|
+
return newQueue;
|
|
155
|
+
};
|
|
156
|
+
const tasks = [];
|
|
157
|
+
records.forEach((rec) => {
|
|
158
|
+
if (rec.state === state) {
|
|
159
|
+
tasks.push(rec.task);
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return tasks;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
//口径对齐 file 档进度条注入闭包(Multithread.js `getRemaining: () => TaskMgr.getTasks('new').length`): 只计 new 队列
|
|
166
|
+
const getRemaining = () => {
|
|
167
|
+
return newQueue.length;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const stats = () => {
|
|
171
|
+
const failedTasks = [];
|
|
172
|
+
let succeeded = 0;
|
|
173
|
+
records.forEach((rec, subid) => {
|
|
174
|
+
if (rec.state === 'finished') {
|
|
175
|
+
succeeded++;
|
|
176
|
+
} else if (rec.state === 'finished_with_errors') {
|
|
177
|
+
failedTasks.push(subid);
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
taskFolder,
|
|
182
|
+
succeeded,
|
|
183
|
+
failed: failedTasks.length,
|
|
184
|
+
failedTasks,
|
|
185
|
+
succButSaveFailed: messageSubids.size,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
//按完成顺序遍历某一侧(hasError=false/true)结果, cb(count, task, result);
|
|
190
|
+
//任务当前不在对应终态(如重试中)时 task 传 null, 对齐 file 档任务文件缺失语义;
|
|
191
|
+
//result 存的是 undefined 时原样传 undefined, 不跳过
|
|
192
|
+
const forEachSide = (hasError, state, cb) => {
|
|
193
|
+
let count = 0;
|
|
194
|
+
finishedOrder.forEach((subid) => {
|
|
195
|
+
const entry = results.get(subid);
|
|
196
|
+
if (!entry || entry.hasError !== hasError) {
|
|
197
|
+
return;
|
|
198
|
+
};
|
|
199
|
+
const rec = records.get(subid);
|
|
200
|
+
const task = rec && rec.state === state ? rec.task : null;
|
|
201
|
+
cb(count, task, entry.data);
|
|
202
|
+
count++;
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const forEachSuccResult = (cb) => {
|
|
207
|
+
forEachSide(false, 'finished', cb);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const forEachErrorResult = (cb) => {
|
|
211
|
+
forEachSide(true, 'finished_with_errors', cb);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const forEachResult = (cb) => {//先 succ(succeeded=true) 后 errors(false), count 两段连续编号
|
|
215
|
+
let offset = 0;
|
|
216
|
+
forEachSide(false, 'finished', (count, task, result) => {
|
|
217
|
+
cb(count, task, result, true);
|
|
218
|
+
offset = count + 1;
|
|
219
|
+
});
|
|
220
|
+
forEachSide(true, 'finished_with_errors', (count, task, err) => {
|
|
221
|
+
cb(offset + count, task, err, false);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
add,
|
|
227
|
+
addMany,
|
|
228
|
+
pop,
|
|
229
|
+
saveLog,
|
|
230
|
+
saveResult,
|
|
231
|
+
deleteResult,
|
|
232
|
+
move,
|
|
233
|
+
incrementRetry,
|
|
234
|
+
addMessage,
|
|
235
|
+
clearMessages,
|
|
236
|
+
getSavedResultType,
|
|
237
|
+
getState,
|
|
238
|
+
getTasks,
|
|
239
|
+
getRemaining,
|
|
240
|
+
stats,
|
|
241
|
+
forEachSuccResult,
|
|
242
|
+
forEachErrorResult,
|
|
243
|
+
forEachResult,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
module.exports = { createMemoryStore };
|