multi-tasks 3.5.2 → 4.0.0-rc.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.
@@ -1,247 +1,337 @@
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 };
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, snapshot) => {
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
+ //newQueue getTasks('new') 返回本体的活数组。pop/move 不物理移除(shift/splice 每次 O(n),
18
+ //10 万任务派发与 aof 重放都摊成 O(N²)): 头部指针 newHead + 墓碑化(条目靠 isAlive 判定存活),
19
+ //失效条目由 pop 跳过、compactNewQueue 定期原地回收(splice 原地, 保住返回本体契约)
20
+ const newQueue = [];
21
+ let newHead = 0;
22
+ let newLiveCount = 0;//存活条目数(getRemaining 口径; newQueue.length 含已派/墓碑, 不能直接用)
23
+ const finishedOrder = [];//subid 首次进入终态的顺序, forEach 按它遍历
24
+ const finishedSet = new Set();//finishedOrder 伴生集合: 判重 O(1)(includes 是 O(n), 每任务一次终态 move 累计 O(N²))
25
+ const results = new Map();//subid -> {hasError, data}
26
+ const logs = new Map();//subid -> log
27
+ const messages = new Map();//文件名 `<type>.<subid>.json` -> message
28
+ const messageSubids = new Set();//有消息的去重 subid 集合, stats().succButSaveFailed 口径(对齐 TaskMgr 一任务多消息只计一次)
29
+
30
+ //aof checkpoint 恢复: snapshot __serialize() JSON 读回形态;
31
+ //newQueue subid 序列, 恢复时按 records 取回同一 task 对象引用(pop/move 依赖引用一致)
32
+ if (snapshot) {
33
+ (snapshot.records || []).forEach(([subid, rec]) => { records.set(subid, rec); });
34
+ (snapshot.newQueue || []).forEach((subid) => {
35
+ const rec = records.get(subid);
36
+ if (rec) {
37
+ newQueue.push(rec.task);
38
+ newLiveCount++;//snapshot.newQueue 只含存活 subid(__serialize 已过滤), 全部计入
39
+ };
40
+ });
41
+ (snapshot.finishedOrder || []).forEach((subid) => { finishedOrder.push(subid); finishedSet.add(subid); });
42
+ (snapshot.results || []).forEach(([subid, entry]) => { results.set(subid, entry); });
43
+ (snapshot.logs || []).forEach(([subid, log]) => { logs.set(subid, log); });
44
+ (snapshot.messages || []).forEach(([fname, message]) => {
45
+ messages.set(fname, message);
46
+ messageSubids.add(message.subid);
47
+ });
48
+ };
49
+
50
+ const assertState = (state) => {
51
+ if (!VALID_STATES.includes(state)) {
52
+ throw new Error(`MemoryStore: invalid task state "${state}"`);
53
+ };
54
+ };
55
+
56
+ //队列条目存活判定: records 覆盖(重复 add)/state 变更(move/pop)都会让旧条目自然成墓碑, 无需物理剔除
57
+ const isAlive = (task) => {
58
+ const rec = records.get(task.subid);
59
+ return !!rec && rec.state === 'new' && rec.task === task;
60
+ };
61
+
62
+ //原地回收 [0,newHead) 与墓碑条目(splice/length 截断都保住数组本体), getTasks('new') 返回前必走
63
+ const compactNewQueue = () => {
64
+ let w = 0;
65
+ for (let r = newHead; r < newQueue.length; r++) {
66
+ const task = newQueue[r];
67
+ if (isAlive(task)) {
68
+ newQueue[w++] = task;
69
+ };
70
+ };
71
+ newQueue.length = w;
72
+ newHead = 0;
73
+ };
74
+
75
+ //消息文件名规范同 TaskMgr 头部约定: 文件名 = <type>.<subid>.json, type 只允许 [a-z0-9_](不含点)
76
+ const MESSAGE_TYPE_PATTERN = /^[a-z0-9_]+$/;
77
+
78
+ const messageFname = (message) => `${message.type}.${message.subid}.json`;
79
+
80
+ const add = (task) => {
81
+ if (!task.subid) {
82
+ task.masterTaskId = masterTaskId;
83
+ task.subid = `${masterTaskId}--${randomsUtil.get36Id()}`;
84
+ };
85
+ //同 subid 重复 add 按 file 档「覆盖同名文件」语义处理: records 覆盖, 旧队列条目随引用失效
86
+ //自然成墓碑(isAlive 的 rec.task === task 判不上), 无需 findIndex+splice 物理剔除, 计数回吐即可
87
+ const existing = records.get(task.subid);
88
+ if (existing && existing.state === 'new') {
89
+ if (existing.task === task) {
90
+ //同一对象重复 add(仅进程内测试场景, 跨 IPC/重放都是反序列化新对象): 引用相同无法靠墓碑区分,
91
+ //旧条目会被 isAlive 误判存活致双派, 退回物理剔除(罕见路径, O(n) 可接受)
92
+ const index = newQueue.findIndex((item) => item === task);
93
+ if (index >= newHead) {
94
+ newQueue.splice(index, 1);
95
+ };
96
+ };
97
+ newLiveCount--;
98
+ };
99
+ records.set(task.subid, { state: 'new', task });
100
+ newQueue.push(task);
101
+ newLiveCount++;
102
+ return task;
103
+ };
104
+
105
+ const addMany = (tasks) => {
106
+ tasks.forEach(add);
107
+ return tasks;
108
+ };
109
+
110
+ const pop = () => {
111
+ while (newHead < newQueue.length) {
112
+ const task = newQueue[newHead++];
113
+ if (!isAlive(task)) {
114
+ continue;//墓碑: 重复 add 的旧项/已被 move 走的条目
115
+ };
116
+ records.get(task.subid).state = 'running';
117
+ newLiveCount--;
118
+ if (newHead >= 1024 && newHead * 2 >= newQueue.length) {
119
+ compactNewQueue();//头部墓碑积攒过半时原地回收, 防长跑(重试反复入队)数组无限涨
120
+ };
121
+ return task;
122
+ };
123
+ if (newQueue.length > 0) {//耗尽: 尾部全是墓碑, 直接清零
124
+ newQueue.length = 0;
125
+ newHead = 0;
126
+ };
127
+ return null;
128
+ };
129
+
130
+ const saveLog = (subid, log) => {
131
+ logs.set(subid, log);
132
+ };
133
+
134
+ const saveResult = (subid, hasError, data) => {
135
+ results.set(subid, { hasError, data });
136
+ };
137
+
138
+ const deleteResult = (subid) => {
139
+ results.delete(subid);
140
+ };
141
+
142
+ const move = (subid, from, to) => {
143
+ assertState(from);
144
+ assertState(to);
145
+ const rec = records.get(subid);
146
+ if (!rec || rec.state !== from) {
147
+ return false;
148
+ };
149
+ rec.state = to;
150
+ if (from === 'new') {
151
+ //state 变更即让队列条目成墓碑(isAlive 判不上), findIndex+splice——
152
+ //aof 重放每条 move new->running 都扫+搬剩余队列, 10 万任务累计 O()
153
+ newLiveCount--;
154
+ };
155
+ if (to === 'new') {
156
+ newQueue.push(rec.task);
157
+ newLiveCount++;
158
+ };
159
+ if (TERMINAL_STATES.includes(to) && !finishedSet.has(subid)) {
160
+ finishedSet.add(subid);
161
+ finishedOrder.push(subid);
162
+ };
163
+ return true;
164
+ };
165
+
166
+ //对齐 file 档: TaskMgr.incrementTaskRetry running/ 目录里的任务文件, 任务不在 running 即 ENOENT
167
+ //抛错不增计数——此处同样只在 running 态增计数; 崩溃/超时兜底与 worker 重试竞争时(任务已被对方移回
168
+ // new)不得重复增计数, 否则配额被虚耗、且超限分支会给仍在排队的任务种下 stale worker_crash 结果
169
+ const incrementRetry = (subid) => {
170
+ const rec = records.get(subid);
171
+ if (!rec) {
172
+ return 0;
173
+ };
174
+ if (rec.state !== 'running') {
175
+ return rec.task.__retryCount || 0;
176
+ };
177
+ rec.task.__retryCount = (rec.task.__retryCount || 0) + 1;
178
+ return rec.task.__retryCount;
179
+ };
180
+
181
+ const addMessage = (message) => {
182
+ //对齐 TaskMgr.saveUserMessage 口径: type 不合规拒写(只记日志), level 缺省 info, 补 timestamp
183
+ if (!MESSAGE_TYPE_PATTERN.test(message.type)) {
184
+ runLog.error('[Task Error] invalid user message type, skipped', message.type, message.subid);
185
+ return;
186
+ };
187
+ const enriched = { level: 'info', ...message, timestamp: randomsUtil.getDateTimeTxt() };
188
+ messages.set(messageFname(enriched), enriched);
189
+ messageSubids.add(enriched.subid);
190
+ };
191
+
192
+ const clearMessages = (subid) => {
193
+ messages.forEach((message, fname) => {
194
+ if (message.subid === subid) {
195
+ messages.delete(fname);
196
+ };
197
+ });
198
+ messageSubids.delete(subid);
199
+ };
200
+
201
+ const getSavedResultType = (subid) => {
202
+ const entry = results.get(subid);
203
+ if (!entry) {
204
+ return null;
205
+ };
206
+ return entry.hasError ? 'error' : 'succ';
207
+ };
208
+
209
+ const getState = (subid) => {
210
+ const rec = records.get(subid);
211
+ return rec ? rec.state : null;
212
+ };
213
+
214
+ const getTasks = (state) => {
215
+ assertState(state);
216
+ if (state === 'new') {
217
+ compactNewQueue();//先原地回收头部与墓碑条目再返回本体: 内容语义与物理剔除一致, 引用不变
218
+ return newQueue;
219
+ };
220
+ const tasks = [];
221
+ records.forEach((rec) => {
222
+ if (rec.state === state) {
223
+ tasks.push(rec.task);
224
+ };
225
+ });
226
+ return tasks;
227
+ };
228
+
229
+ //口径对齐 file 档进度条注入闭包(Multithread.js `getRemaining: () => TaskMgr.getTasks('new').length`): 只计 new 队列;
230
+ //存活条目计数(newQueue.length 含已派头部与墓碑, 不能用)
231
+ const getRemaining = () => {
232
+ return newLiveCount;
233
+ };
234
+
235
+ const stats = () => {
236
+ const failedTasks = [];
237
+ let succeeded = 0;
238
+ records.forEach((rec, subid) => {
239
+ if (rec.state === 'finished') {
240
+ succeeded++;
241
+ } else if (rec.state === 'finished_with_errors') {
242
+ failedTasks.push(subid);
243
+ };
244
+ });
245
+ return {
246
+ taskFolder,
247
+ succeeded,
248
+ failed: failedTasks.length,
249
+ failedTasks,
250
+ succButSaveFailed: messageSubids.size,
251
+ };
252
+ };
253
+
254
+ //按完成顺序遍历某一侧(hasError=false/true)结果, cb(count, task, result);
255
+ //任务当前不在对应终态(如重试中)时 task 传 null, 对齐 file 档任务文件缺失语义;
256
+ //result 存的是 undefined 时原样传 undefined, 不跳过
257
+ const forEachSide = (hasError, state, cb) => {
258
+ let count = 0;
259
+ finishedOrder.forEach((subid) => {
260
+ const entry = results.get(subid);
261
+ if (!entry || entry.hasError !== hasError) {
262
+ return;
263
+ };
264
+ const rec = records.get(subid);
265
+ const task = rec && rec.state === state ? rec.task : null;
266
+ cb(count, task, entry.data);
267
+ count++;
268
+ });
269
+ };
270
+
271
+ const forEachSuccResult = (cb) => {
272
+ forEachSide(false, 'finished', cb);
273
+ };
274
+
275
+ const forEachErrorResult = (cb) => {
276
+ forEachSide(true, 'finished_with_errors', cb);
277
+ };
278
+
279
+ const forEachResult = (cb) => {//先 succ(succeeded=true) 后 errors(false), count 两段连续编号
280
+ let offset = 0;
281
+ forEachSide(false, 'finished', (count, task, result) => {
282
+ cb(count, task, result, true);
283
+ offset = count + 1;
284
+ });
285
+ forEachSide(true, 'finished_with_errors', (count, task, err) => {
286
+ cb(offset + count, task, err, false);
287
+ });
288
+ };
289
+
290
+ //对齐 file 档 TaskMgr.resetTaskRetry(retry_fails 重跑前清零); 任务不存在时无操作
291
+ const resetRetry = (subid) => {
292
+ const rec = records.get(subid);
293
+ if (rec) {
294
+ rec.task.__retryCount = 0;
295
+ };
296
+ };
297
+
298
+ //aof 档 checkpoint 用: 全量可序列化状态; 恢复走 createMemoryStore(config, snapshot);
299
+ //newQueue 先 compact 再取 subid——序列化必须只含存活条目, 否则墓碑(已完成/已移走任务)进 checkpoint,
300
+ //恢复后 isAlive 虽能判死, 但 checkpoint 体积虚涨且恢复 loop 白推
301
+ const __serialize = () => {
302
+ compactNewQueue();
303
+ return {
304
+ records: [...records],
305
+ newQueue: newQueue.map((task) => task.subid),
306
+ finishedOrder: [...finishedOrder],
307
+ results: [...results],
308
+ logs: [...logs],
309
+ messages: [...messages],
310
+ };
311
+ };
312
+
313
+ return {
314
+ add,
315
+ addMany,
316
+ pop,
317
+ saveLog,
318
+ saveResult,
319
+ deleteResult,
320
+ move,
321
+ incrementRetry,
322
+ addMessage,
323
+ clearMessages,
324
+ getSavedResultType,
325
+ getState,
326
+ getTasks,
327
+ getRemaining,
328
+ stats,
329
+ forEachSuccResult,
330
+ forEachErrorResult,
331
+ forEachResult,
332
+ resetRetry,
333
+ __serialize,
334
+ };
335
+ };
336
+
337
+ module.exports = { createMemoryStore };