multi-tasks 3.5.1 → 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.
- package/README.md +324 -326
- package/bench-storage.svg +15 -11
- package/package.json +1 -1
- package/utils/makedir.js +5 -4
- package/utils/progressCtl.js +7 -0
- package/utils/runLog.js +7 -2
- package/utils/startupBanner.js +5 -0
- package/workers/AofStore.js +349 -0
- package/workers/MemoryStore.js +337 -247
- package/workers/Multithread.js +92 -53
- package/workers/TaskMgr.js +52 -7
- package/workers/asMaster.js +138 -109
- package/workers/helper.js +77 -77
- package/workers/index.js +282 -224
- package/workers/resultFileAppender.js +62 -0
package/workers/Multithread.js
CHANGED
|
@@ -59,11 +59,11 @@ const start = (config)=>{
|
|
|
59
59
|
const masterStartTimestamp = new Date()*1;//本轮 master 启动时间(resume 不追溯首轮)
|
|
60
60
|
let workerCrashCount = 0;//累计崩溃补员次数, 进 onFinish report
|
|
61
61
|
|
|
62
|
-
let
|
|
63
|
-
let getStats = ()=>
|
|
64
|
-
let getRemaining = ()=>
|
|
62
|
+
let taskStore = null;//memory/aof 档: 任务状态/结果在 master 内存(file 档恒为 null, 走 TaskMgr 磁盘链路)
|
|
63
|
+
let getStats = ()=> taskStore ? taskStore.stats() : TaskMgr.getFinalStats();
|
|
64
|
+
let getRemaining = ()=> taskStore ? taskStore.getRemaining() : TaskMgr.getTasks('new').length;
|
|
65
65
|
|
|
66
|
-
//memory 档 worker 存储操作的应用点: 只在 __storageOp 分支的 queue.enqueue 里调(与派发同一串行通道,
|
|
66
|
+
//memory/aof 档 worker 存储操作的应用点: 只在 __storageOp 分支的 queue.enqueue 里调(与派发同一串行通道,
|
|
67
67
|
//同 worker 的操作消息排在其后续领新请求之前, pop 时 Map 必已含该 worker 此前创建的全部任务)
|
|
68
68
|
let applyStorageOp = (worker, message)=>{
|
|
69
69
|
let op = message.op;
|
|
@@ -76,19 +76,19 @@ const start = (config)=>{
|
|
|
76
76
|
try{
|
|
77
77
|
switch(op){
|
|
78
78
|
case 'createTask':
|
|
79
|
-
|
|
79
|
+
taskStore.add(args[0]);
|
|
80
80
|
break;
|
|
81
|
-
case 'saveLog':
|
|
82
|
-
case 'saveResult':
|
|
83
|
-
case 'deleteResult':
|
|
81
|
+
case 'saveLog': taskStore.saveLog(args[0], args[1]); break;
|
|
82
|
+
case 'saveResult': taskStore.saveResult(args[0], args[1], args[2]); break;
|
|
83
|
+
case 'deleteResult': taskStore.deleteResult(args[0]); break;
|
|
84
84
|
case 'mvTask': {
|
|
85
|
-
let succ =
|
|
85
|
+
let succ = taskStore.move(args[0], args[1], args[2]);
|
|
86
86
|
ackValue = {newPath: `${args[2]}/${args[0]}.task.json`, succ};
|
|
87
87
|
break;
|
|
88
88
|
};
|
|
89
|
-
case 'incrementRetry': ackValue =
|
|
90
|
-
case 'saveUserMessage':
|
|
91
|
-
case 'deleteUserMessage':
|
|
89
|
+
case 'incrementRetry': ackValue = taskStore.incrementRetry(args[0]); break;
|
|
90
|
+
case 'saveUserMessage': taskStore.addMessage(args[0]); break;
|
|
91
|
+
case 'deleteUserMessage': taskStore.clearMessages(args[0]); break;
|
|
92
92
|
default:
|
|
93
93
|
throw new Error(`unknown storage op "${op}"`);
|
|
94
94
|
};
|
|
@@ -129,46 +129,54 @@ const start = (config)=>{
|
|
|
129
129
|
process.exit(0);
|
|
130
130
|
}, 10);
|
|
131
131
|
};
|
|
132
|
-
if(
|
|
133
|
-
//memory 档: 收尾排进派发同一串行队列(append-and-return, 绝不阻塞等队列——本函数在
|
|
132
|
+
if(taskStore){
|
|
133
|
+
//memory/aof 档: 收尾排进派发同一串行队列(append-and-return, 绝不阻塞等队列——本函数在
|
|
134
134
|
//shouldTerminate 路径就是在队列任务内被调用的, 阻塞 await 队列即死锁): 已到达未应用的
|
|
135
|
-
//worker 存储操作先于收尾计数应用; 此后排队的已死 worker 操作按 terminate
|
|
135
|
+
//worker 存储操作先于收尾计数应用; 此后排队的已死 worker 操作按 terminate 语义丢弃。
|
|
136
|
+
//aof 档先 await 最终 checkpoint 再写计数器行: await 的只是本 enqueue 自身 promise
|
|
137
|
+
//(此前项已随队列推进完毕), 不构成「等队列 drain」。
|
|
136
138
|
queue.enqueue(async ()=>{
|
|
139
|
+
if(typeof taskStore.checkpoint === 'function'){
|
|
140
|
+
await taskStore.checkpoint();
|
|
141
|
+
};
|
|
142
|
+
if(typeof taskStore.flushResults === 'function'){
|
|
143
|
+
await taskStore.flushResults();//aof 档: 内存结果全量物化到 results/ 完再写计数器行
|
|
144
|
+
};
|
|
137
145
|
writeCounterAndExit();
|
|
138
146
|
});
|
|
139
147
|
return;
|
|
140
148
|
};
|
|
141
149
|
writeCounterAndExit();
|
|
142
150
|
};
|
|
143
|
-
let
|
|
144
|
-
//memory 档: 与 file 档同语义同顺序(先结果后迁移); 只在 queue.enqueue 里调(与 worker 存储操作
|
|
151
|
+
let finalizeTimeoutTaskInStore = (subTask, cost)=>{
|
|
152
|
+
//memory/aof 档: 与 file 档同语义同顺序(先结果后迁移); 只在 queue.enqueue 里调(与 worker 存储操作
|
|
145
153
|
//同一串行通道): 该 worker 已到达未应用的 saveResult 等操作必先于本判定应用, 已交卷任务不会被误杀
|
|
146
154
|
let subid = subTask.subid;
|
|
147
155
|
try{
|
|
148
|
-
let resultType =
|
|
156
|
+
let resultType = taskStore.getSavedResultType(subid);
|
|
149
157
|
if(resultType === 'succ'){//超时边界: worker 已成功交卷, 视为已完成
|
|
150
|
-
|
|
158
|
+
taskStore.move(subid, 'running', 'finished');
|
|
151
159
|
}else if(resultType === 'error'){//超时边界: worker 已自行记错误
|
|
152
|
-
|
|
160
|
+
taskStore.move(subid, 'running', 'finished_with_errors');
|
|
153
161
|
}else{
|
|
154
162
|
if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
155
|
-
let retryCount =
|
|
163
|
+
let retryCount = taskStore.incrementRetry(subid);
|
|
156
164
|
if(retryCount <= config.maxTaskRetries){
|
|
157
165
|
runLog.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
158
|
-
|
|
166
|
+
taskStore.move(subid, 'running', 'new');
|
|
159
167
|
return;
|
|
160
168
|
};
|
|
161
169
|
};
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
taskStore.saveResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
|
|
171
|
+
taskStore.move(subid, 'running', 'finished_with_errors');
|
|
164
172
|
}
|
|
165
173
|
}catch(e){
|
|
166
174
|
runLog.warn('[Master]: failed to finalize timeout task', subid, e);
|
|
167
175
|
}
|
|
168
176
|
};
|
|
169
177
|
let finalizeTimeoutTask = (subTask, cost)=>{
|
|
170
|
-
if(
|
|
171
|
-
queue.enqueue(async ()=>{
|
|
178
|
+
if(taskStore){
|
|
179
|
+
queue.enqueue(async ()=>{ finalizeTimeoutTaskInStore(subTask, cost); });
|
|
172
180
|
return;
|
|
173
181
|
};
|
|
174
182
|
let subid = subTask.subid;
|
|
@@ -204,19 +212,19 @@ const start = (config)=>{
|
|
|
204
212
|
worker.kill();//不 disconnect: exit 事件会按崩溃补员
|
|
205
213
|
finalizeTimeoutTask(subTask, cost);
|
|
206
214
|
};
|
|
207
|
-
let
|
|
208
|
-
//memory 档: 与 file 档同语义(重试移回 new / 超限记 worker_crash 归档),
|
|
215
|
+
let handleCrashedTaskInStore = (subTask)=>{
|
|
216
|
+
//memory/aof 档: 与 file 档同语义(重试移回 new / 超限记 worker_crash 归档), 操作走 master 侧 store;
|
|
209
217
|
//只在 queue.enqueue 里调: 该 worker 崩溃前已到达的存储操作必先应用, 再判定重试/归档
|
|
210
218
|
let subid = subTask.subid;
|
|
211
219
|
try{
|
|
212
|
-
let retryCount =
|
|
220
|
+
let retryCount = taskStore.incrementRetry(subid);
|
|
213
221
|
if(retryCount <= config.maxTaskRetries){
|
|
214
222
|
runLog.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
|
|
215
|
-
|
|
223
|
+
taskStore.move(subid, 'running', 'new');
|
|
216
224
|
}else{
|
|
217
225
|
runLog.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
taskStore.saveResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
|
|
227
|
+
taskStore.move(subid, 'running', 'finished_with_errors');
|
|
220
228
|
};
|
|
221
229
|
}catch(e){
|
|
222
230
|
runLog.warn('[Master]: failed to handle crashed task', subid, e);
|
|
@@ -224,8 +232,8 @@ const start = (config)=>{
|
|
|
224
232
|
};
|
|
225
233
|
let handleCrashedTask = (subTask)=>{
|
|
226
234
|
if(!(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0)) return;
|
|
227
|
-
if(
|
|
228
|
-
queue.enqueue(async ()=>{
|
|
235
|
+
if(taskStore){
|
|
236
|
+
queue.enqueue(async ()=>{ handleCrashedTaskInStore(subTask); });
|
|
229
237
|
return;
|
|
230
238
|
};
|
|
231
239
|
let subid = subTask.subid;
|
|
@@ -289,8 +297,15 @@ const start = (config)=>{
|
|
|
289
297
|
}
|
|
290
298
|
return;
|
|
291
299
|
}
|
|
292
|
-
if(
|
|
293
|
-
queue.enqueue(async ()=>{
|
|
300
|
+
if(taskStore && message && message.__storageOp){//memory/aof 档 worker 存储操作: 与派发同一串行通道逐条应用, 禁止捎带合批
|
|
301
|
+
queue.enqueue(async ()=>{
|
|
302
|
+
applyStorageOp(worker, message);
|
|
303
|
+
//aof 档周期 checkpoint: 与 op 应用同一串行任务内触发并 await, snapshot 才对应 aof
|
|
304
|
+
//前缀一致态(不 await 则后续 op 先进队列应用, 截断时其 aof 行被抹掉); memory 档无此方法, 跳过
|
|
305
|
+
if(typeof taskStore.maybeCheckpoint === 'function'){
|
|
306
|
+
await taskStore.maybeCheckpoint();
|
|
307
|
+
};
|
|
308
|
+
});
|
|
294
309
|
return;
|
|
295
310
|
}
|
|
296
311
|
if(message === MSG_REQUEST_NEW_TASK){
|
|
@@ -298,10 +313,10 @@ const start = (config)=>{
|
|
|
298
313
|
progressCtl.taskCompleted(worker.id);//worker 行回到 idle; 完成数按盘上结果目录统计, 不在此计数
|
|
299
314
|
queue.enqueue(async () => {
|
|
300
315
|
let subTask = null;
|
|
301
|
-
if(
|
|
302
|
-
//memory 档:
|
|
303
|
-
|
|
304
|
-
subTask =
|
|
316
|
+
if(taskStore){
|
|
317
|
+
//memory/aof 档: 任务全在 master 侧 store 队列, pop 即出队(aof 档 pop 落 aof 行);
|
|
318
|
+
//无需磁盘重读兜底(动态任务经 __storageOp 同一串行通道进来, pop 时必然已可见)
|
|
319
|
+
subTask = taskStore.pop();
|
|
305
320
|
}else{
|
|
306
321
|
//use queue to make sure no I/O conflict; update/pop/判空须在同一个串行任务里原子完成
|
|
307
322
|
await TaskMgr.updateNewTasks();
|
|
@@ -375,7 +390,7 @@ const start = (config)=>{
|
|
|
375
390
|
let initPromise = config.__resume ? asMaster.resume(config) : asMaster.init(config);
|
|
376
391
|
initPromise.then((inited)=>{
|
|
377
392
|
config = inited.config;
|
|
378
|
-
|
|
393
|
+
taskStore = inited.store || null;
|
|
379
394
|
//一次性启动块(走 console, 在劫持前上屏): 标题框横幅 + 平铺关键字段, 模式由 __ 标记推导;
|
|
380
395
|
//init 成功后所有信息齐备(new tasks 数/masterTaskId/__sysLogFile), 一次打完
|
|
381
396
|
let startupInfo = {
|
|
@@ -386,6 +401,8 @@ const start = (config)=>{
|
|
|
386
401
|
numCPUs,
|
|
387
402
|
pid: process.pid,
|
|
388
403
|
newTasks: getRemaining(),
|
|
404
|
+
taskStorage: config.task_storage,
|
|
405
|
+
storageDefaultApplied: !!config.__storageDefaultApplied,
|
|
389
406
|
sysLogFile: config.__sysLogFile,
|
|
390
407
|
startedAt: randomsUtil.getDateTimeTxt(),
|
|
391
408
|
};
|
|
@@ -452,12 +469,12 @@ const start = (config)=>{
|
|
|
452
469
|
workers: {count: numOfWorkers, crashed: workerCrashCount},
|
|
453
470
|
};
|
|
454
471
|
let onFinishHelper;
|
|
455
|
-
if(
|
|
456
|
-
//memory 档:
|
|
472
|
+
if(taskStore){
|
|
473
|
+
//memory/aof 档: 结果在 master 侧 store(aof 档收尾物化 results/ 文件, 内容一致), 遍历接口委托 taskStore(与 TaskMgr 同签名同口径)
|
|
457
474
|
onFinishHelper = {
|
|
458
|
-
foreachSuccResult: (cb) =>
|
|
459
|
-
foreachErrorResult: (cb) =>
|
|
460
|
-
foreachResult: (cb) =>
|
|
475
|
+
foreachSuccResult: (cb) => taskStore.forEachSuccResult(cb),
|
|
476
|
+
foreachErrorResult: (cb) => taskStore.forEachErrorResult(cb),
|
|
477
|
+
foreachResult: (cb) => taskStore.forEachResult(cb),
|
|
461
478
|
};
|
|
462
479
|
}else{
|
|
463
480
|
onFinishHelper = {
|
|
@@ -466,7 +483,15 @@ const start = (config)=>{
|
|
|
466
483
|
foreachResult: (cb) => TaskMgr.forEachResult(cb),
|
|
467
484
|
};
|
|
468
485
|
}
|
|
469
|
-
|
|
486
|
+
//onFinish 是用户回调: 同步抛错/异步 reject 都只告警, 不得中断收尾——
|
|
487
|
+
//memory/aof 档本函数跑在串行 queue 里, 抛错会被 queue 吞掉, 计数器行与 process.exit 永远不到, master 挂死
|
|
488
|
+
try{
|
|
489
|
+
Promise.resolve(USER_CONFIG.onFinish(report, onFinishHelper)).catch((e)=>{
|
|
490
|
+
runLog.warn('[Master]: onFinish callback rejected, continue finishing', e);
|
|
491
|
+
});
|
|
492
|
+
}catch(e){
|
|
493
|
+
runLog.warn('[Master]: onFinish callback threw, continue finishing', e);
|
|
494
|
+
};
|
|
470
495
|
delete USER_CONFIG.onFinish;
|
|
471
496
|
}
|
|
472
497
|
//计数器行先进 run.log(progressBar 未接管终端时 echo 上屏, 接管时由 progressCtl.finish() 打在屏幕);
|
|
@@ -479,10 +504,16 @@ const start = (config)=>{
|
|
|
479
504
|
process.exit(0);
|
|
480
505
|
}, 10);
|
|
481
506
|
};
|
|
482
|
-
if(
|
|
483
|
-
//memory 档: 同 terminateAllAndExit 口径, 收尾排进派发串行队列, 队列里已到达的存储操作
|
|
484
|
-
//先应用再统计; file 档维持原同步收尾一字不动
|
|
507
|
+
if(taskStore){
|
|
508
|
+
//memory/aof 档: 同 terminateAllAndExit 口径, 收尾排进派发串行队列, 队列里已到达的存储操作
|
|
509
|
+
//先应用再统计; aof 档先 await 最终 checkpoint(snapshot+截断落盘)再收尾; file 档维持原同步收尾一字不动
|
|
485
510
|
queue.enqueue(async ()=>{
|
|
511
|
+
if(typeof taskStore.checkpoint === 'function'){
|
|
512
|
+
await taskStore.checkpoint();
|
|
513
|
+
};
|
|
514
|
+
if(typeof taskStore.flushResults === 'function'){
|
|
515
|
+
await taskStore.flushResults();//aof 档: 内存结果全量物化到 results/ 完再收尾
|
|
516
|
+
};
|
|
486
517
|
finishAll();
|
|
487
518
|
});
|
|
488
519
|
}else{
|
|
@@ -528,9 +559,9 @@ function subWorker(config){
|
|
|
528
559
|
}else
|
|
529
560
|
if(message.subTask){//new task
|
|
530
561
|
let {config, subTask, startTimestamp, taskCount} = message;
|
|
531
|
-
//memory 档 worker 不碰任务目录: 存储后端换 IpcTaskStore(逐条 {__storageOp} 发 master),
|
|
562
|
+
//memory/aof 档 worker 不碰任务目录: 存储后端换 IpcTaskStore(逐条 {__storageOp} 发 master),
|
|
532
563
|
//操作序列与 file 档逐条同构; file 档后端即 TaskMgr(磁盘, 现状不变)
|
|
533
|
-
let store = config.task_storage === '
|
|
564
|
+
let store = config.task_storage === 'file' ? TaskMgr : IpcTaskStore;
|
|
534
565
|
store.load(config);
|
|
535
566
|
//用户任务自带的 index/taskCount 保留原值(重试/续跑后任务身份不变),
|
|
536
567
|
//未提供时才填全局派发计数器(v1 兼容)
|
|
@@ -610,7 +641,15 @@ function runTask(config, subTask, subTaskLog, store){
|
|
|
610
641
|
if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
611
642
|
//重试决策在 worker(file 档如此, memory 档同构勿收归 master); await 两档通用:
|
|
612
643
|
//file 档 incrementTaskRetry 是同步值, memory 档是 needAck 回执 Promise——worker 侧唯一 async 化点
|
|
613
|
-
|
|
644
|
+
//失败保护: 回执 {ok:false} reject / file 档读文件 ENOENT throw 时, 不包则 finally 链尾部无人接
|
|
645
|
+
//(worker unhandled rejection 崩进程, resolve 永远不到)——按「放弃本次重试, 落错误结果」继续, resolve 必达
|
|
646
|
+
let retryCount;
|
|
647
|
+
try{
|
|
648
|
+
retryCount = await store.incrementTaskRetry(subTask.subid);
|
|
649
|
+
}catch(e){
|
|
650
|
+
runLog.warn(`[Task Retry] "${subTask.subid}" failed to increment retry count, give up retrying`, e);
|
|
651
|
+
retryCount = config.maxTaskRetries + 1;
|
|
652
|
+
};
|
|
614
653
|
if(retryCount <= config.maxTaskRetries){
|
|
615
654
|
runLog.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
616
655
|
retried = true;
|
package/workers/TaskMgr.js
CHANGED
|
@@ -15,6 +15,22 @@ let allSubTasks = {
|
|
|
15
15
|
|
|
16
16
|
let main_config;
|
|
17
17
|
|
|
18
|
+
//initialTasks 播种批量写的并发上限(createNewTasks 专用)
|
|
19
|
+
const SEED_WRITE_CONCURRENCY = 128;
|
|
20
|
+
|
|
21
|
+
//建任务文件的准备(赋 subid + 序列化): createNewTask(单条同步写)与 createNewTasks(批量并发写)共用
|
|
22
|
+
const _prepareNewTask = (task)=>{
|
|
23
|
+
const {masterTaskId, taskFolderNew} = main_config;
|
|
24
|
+
const subid = randomsUtil.get36Id();
|
|
25
|
+
|
|
26
|
+
task.masterTaskId = masterTaskId;
|
|
27
|
+
task.subid = `${masterTaskId}--${subid}`;
|
|
28
|
+
return {
|
|
29
|
+
fpath: pathutil.resolve(taskFolderNew, `./${task.subid}.task.json`),
|
|
30
|
+
content: JSON.stringify(task),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
18
34
|
//.sys/user_messages 消息文件命名规范(严谨, 解析依赖以下两条不变量):
|
|
19
35
|
// 文件名 = <type>.<subid>.json; type 是库内机器常量, 只允许 [a-z0-9_](必须不含点), 写入时有同样校验;
|
|
20
36
|
// subid 是首个点之后的全部(可含点, 非空)。type 无点 ⇒ 按首个点切分无歧义
|
|
@@ -105,13 +121,42 @@ const TaskMgr = {
|
|
|
105
121
|
return main_config;
|
|
106
122
|
},
|
|
107
123
|
createNewTask:(task)=>{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
let {fpath, content} = _prepareNewTask(task);
|
|
125
|
+
fs.writeFileSync(fpath, content);
|
|
126
|
+
},
|
|
127
|
+
//批量建任务(initialTasks 播种专用): 与 createNewTask 同规则赋 subid, 有界并发异步写,
|
|
128
|
+
//全部写完才 resolve(任一写失败即 reject, 走 init 异步失败 FATAL exit(1) 链路);
|
|
129
|
+
//worker 动态建任务仍走 createNewTask 单条同步立即写(master 靠强制重读磁盘发现, 不能异步/缓冲)
|
|
130
|
+
createNewTasks:(tasks)=>{
|
|
131
|
+
let items = tasks.map((task)=>_prepareNewTask(task));
|
|
132
|
+
return new Promise((resolve, reject)=>{
|
|
133
|
+
let cursor = 0;
|
|
134
|
+
let active = 0;
|
|
135
|
+
let failed = false;
|
|
136
|
+
const launch = ()=>{
|
|
137
|
+
if(failed) return;
|
|
138
|
+
if(cursor >= items.length){
|
|
139
|
+
if(active === 0) resolve();
|
|
140
|
+
return;
|
|
141
|
+
};
|
|
142
|
+
while(active < SEED_WRITE_CONCURRENCY && cursor < items.length){
|
|
143
|
+
let item = items[cursor++];
|
|
144
|
+
active++;
|
|
145
|
+
fs.writeFile(item.fpath, item.content, (err)=>{
|
|
146
|
+
active--;
|
|
147
|
+
if(err){
|
|
148
|
+
if(!failed){
|
|
149
|
+
failed = true;
|
|
150
|
+
reject(err);
|
|
151
|
+
};
|
|
152
|
+
return;
|
|
153
|
+
};
|
|
154
|
+
launch();
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
launch();
|
|
159
|
+
});
|
|
115
160
|
},
|
|
116
161
|
loadAllTasks:(loadtype)=>{
|
|
117
162
|
const {progressRootFolder} = main_config;
|
package/workers/asMaster.js
CHANGED
|
@@ -1,110 +1,139 @@
|
|
|
1
|
-
var fs = require('fs');
|
|
2
|
-
var pathUtil = require('path');
|
|
3
|
-
|
|
4
|
-
const TaskMgr = require('./TaskMgr');
|
|
5
|
-
const { createMemoryStore } = require('./MemoryStore');
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
const
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
config =
|
|
88
|
-
|
|
89
|
-
if(config.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var pathUtil = require('path');
|
|
3
|
+
|
|
4
|
+
const TaskMgr = require('./TaskMgr');
|
|
5
|
+
const { createMemoryStore } = require('./MemoryStore');
|
|
6
|
+
const { createAofStore, loadFromDisk } = require('./AofStore');
|
|
7
|
+
const { attachResultAppender } = require('./resultFileAppender');
|
|
8
|
+
const randomsUtil = require('../utils/randoms');
|
|
9
|
+
const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
|
|
10
|
+
|
|
11
|
+
const resume = (config)=>{
|
|
12
|
+
config.__resume = true;
|
|
13
|
+
let {taskRootFolder, taskId} = config;
|
|
14
|
+
let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
|
|
15
|
+
|
|
16
|
+
//load from config file: 旧存档作底(兼作任务目录合法性校验, 不存在抛 ENOENT),
|
|
17
|
+
//本次调用传入的配置覆盖同名字段——存档只补未传入的字段
|
|
18
|
+
let task_config = JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json')));
|
|
19
|
+
Object.keys(task_config).forEach((key)=>{
|
|
20
|
+
if(key.startsWith('__')) delete task_config[key];//读侧剔除一次性标记, 兼容 <=3.1.3 旧存档
|
|
21
|
+
});
|
|
22
|
+
config = {
|
|
23
|
+
...task_config,
|
|
24
|
+
...config
|
|
25
|
+
};
|
|
26
|
+
config.masterTaskId = taskId;
|
|
27
|
+
|
|
28
|
+
TaskMgr.load(config);
|
|
29
|
+
config = TaskMgr.initFolders(config);
|
|
30
|
+
|
|
31
|
+
if(config.task_storage === 'aof'){
|
|
32
|
+
//aof 档 resume: snapshot + aof 增量重放重建内存态, 语义与 file 档逐项同构:
|
|
33
|
+
//running 全量回 new(resume 不重置重试计数); retry_fails 再把 finished_with_errors 回 new
|
|
34
|
+
//并重置计数/清消息。回退动作本身记 aof 行, 保持「重放=真相」。
|
|
35
|
+
const store = loadFromDisk(config);
|
|
36
|
+
store.getTasks('running').forEach((task)=>{
|
|
37
|
+
store.move(task.subid, 'running', 'new');
|
|
38
|
+
});
|
|
39
|
+
if(config.__retryFails){
|
|
40
|
+
store.getTasks('finished_with_errors').forEach((task)=>{
|
|
41
|
+
store.move(task.subid, 'finished_with_errors', 'new');
|
|
42
|
+
store.resetRetry(task.subid);
|
|
43
|
+
store.clearMessages(task.subid);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
console.log('******resume multi-thread');
|
|
47
|
+
return Promise.resolve({config, store});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve, reject)=>{
|
|
51
|
+
|
|
52
|
+
TaskMgr.getTaskFiles('running', (taskfiles)=>{
|
|
53
|
+
TaskMgr.getTaskFiles('finished_with_errors', (errfiles)=>{
|
|
54
|
+
let promises = [];
|
|
55
|
+
taskfiles.forEach((fname)=>{
|
|
56
|
+
let subid = fname.replace(/\.task\.json$/, '');
|
|
57
|
+
promises.push(TaskMgr.mvTaskPromise(subid, 'running', 'new'));
|
|
58
|
+
});
|
|
59
|
+
if(config.__retryFails){
|
|
60
|
+
errfiles.forEach((fname)=>{
|
|
61
|
+
let subid = fname.replace(/\.task\.json$/, '');
|
|
62
|
+
promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
|
|
63
|
+
TaskMgr.resetTaskRetry(subid);//retry_fails 重跑失败任务时重置重试计数(resume 不重置)
|
|
64
|
+
TaskMgr.deleteUserMessage(subid);//重跑即新一轮: 清掉该任务在 .sys/user_messages 的遗留消息
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
Promise.all(promises).then(()=>{
|
|
69
|
+
TaskMgr.loadAllTasks().then((types)=>{
|
|
70
|
+
console.log('******resume multi-thread')
|
|
71
|
+
setTimeout(()=>{
|
|
72
|
+
resolve({config, store: null});//memory 档被拦截, aof 档上方已分支返回, 到这里的都是 file 档
|
|
73
|
+
}, 1000);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
}).catch((e)=>{
|
|
77
|
+
reject(e);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const init = (config)=>{
|
|
87
|
+
if(!config) config = {};
|
|
88
|
+
if(!config.initialTasks){ throw new Error('Please provide initialTasks data!'); };
|
|
89
|
+
if(!config.processTask){ throw new Error('Please provide processTask function!'); };
|
|
90
|
+
|
|
91
|
+
config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
|
|
92
|
+
|
|
93
|
+
let {taskRootFolder, taskName} = config;
|
|
94
|
+
|
|
95
|
+
if(taskName && !taskName.match(/^[a-zA-Z0-9_-]+$/)){ throw new Error('Error: taskName can only contain letters and numbers'); }
|
|
96
|
+
if(!taskName){ taskName = 'multitasks'; }
|
|
97
|
+
|
|
98
|
+
let {round, pow, random} = Math;
|
|
99
|
+
let dateTimeText = randomsUtil.getDateTimeTxt();
|
|
100
|
+
let randomNum = round((random() * pow(10,8)),8).toString(36);
|
|
101
|
+
let masterTaskId = config.taskId ? config.taskId : `${taskName}${dateTimeText}--${randomNum}`;
|
|
102
|
+
|
|
103
|
+
config.taskRootFolder = taskRootFolder;
|
|
104
|
+
config.masterTaskId = masterTaskId;
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
TaskMgr.load(config);
|
|
108
|
+
config = TaskMgr.initFolders(config);
|
|
109
|
+
|
|
110
|
+
if(config.task_storage === 'memory'){
|
|
111
|
+
//memory 档: 不建任务文件(目录与 task_config.json 照建照写), 播种进 master 内存存储
|
|
112
|
+
const store = createMemoryStore(config);
|
|
113
|
+
store.addMany(config.initialTasks);
|
|
114
|
+
if(config.memory_append_result_files === true){
|
|
115
|
+
//内部开关 memory_append_result_files(不对外文档化): memory 档也追加结果聚合文件(只追加不重写)
|
|
116
|
+
attachResultAppender(store, config.taskFolder);
|
|
117
|
+
};
|
|
118
|
+
return Promise.resolve({config, store});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if(config.task_storage === 'aof'){
|
|
122
|
+
//aof 档: 与 memory 档同形——不建任务文件, 播种进 AofStore(内存+aof 追加)
|
|
123
|
+
const store = createAofStore(config);
|
|
124
|
+
store.addMany(config.initialTasks);
|
|
125
|
+
return Promise.resolve({config, store});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
//批量并发写播种(initialTasks 量大时同步逐条 writeFileSync 会长时间阻塞 master 启动)
|
|
129
|
+
return TaskMgr.createNewTasks(config.initialTasks).then(()=>{
|
|
130
|
+
//to reload is for to reStart a collapsed task
|
|
131
|
+
return TaskMgr.loadNewTasks().then(()=>{
|
|
132
|
+
return {config, store: null};
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
module.exports = {
|
|
137
|
+
init,
|
|
138
|
+
resume
|
|
110
139
|
};
|