multi-tasks 3.4.0 → 3.5.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.
@@ -7,6 +7,7 @@ let MSG_NO_TASK_FOUND = 'no_task_found';
7
7
 
8
8
  const asMaster = require('./asMaster');
9
9
  const TaskMgr = require('./TaskMgr');
10
+ const IpcTaskStore = require('./IpcTaskStore');
10
11
  const WorkerMgr = require('./WorkerMgr');
11
12
  const helper = require('./helper');
12
13
  const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
@@ -58,6 +59,53 @@ const start = (config)=>{
58
59
  const masterStartTimestamp = new Date()*1;//本轮 master 启动时间(resume 不追溯首轮)
59
60
  let workerCrashCount = 0;//累计崩溃补员次数, 进 onFinish report
60
61
 
62
+ let memoryStore = null;//memory 档: 任务状态/结果全在 master 内存(file 档恒为 null, 走 TaskMgr 磁盘链路)
63
+ let getStats = ()=> memoryStore ? memoryStore.stats() : TaskMgr.getFinalStats();
64
+ let getRemaining = ()=> memoryStore ? memoryStore.getRemaining() : TaskMgr.getTasks('new').length;
65
+
66
+ //memory 档 worker 存储操作的应用点: 只在 __storageOp 分支的 queue.enqueue 里调(与派发同一串行通道,
67
+ //同 worker 的操作消息排在其后续领新请求之前, pop 时 Map 必已含该 worker 此前创建的全部任务)
68
+ let applyStorageOp = (worker, message)=>{
69
+ let op = message.op;
70
+ let args = (message.args || []).map((value)=>{
71
+ return (value && value[IpcTaskStore.__UNDEFINED_SENTINEL]) ? undefined : value;
72
+ });
73
+ let ackValue;
74
+ let ok = true;
75
+ let error = null;
76
+ try{
77
+ switch(op){
78
+ case 'createTask':
79
+ memoryStore.add(args[0]);
80
+ break;
81
+ case 'saveLog': memoryStore.saveLog(args[0], args[1]); break;
82
+ case 'saveResult': memoryStore.saveResult(args[0], args[1], args[2]); break;
83
+ case 'deleteResult': memoryStore.deleteResult(args[0]); break;
84
+ case 'mvTask': {
85
+ let succ = memoryStore.move(args[0], args[1], args[2]);
86
+ ackValue = {newPath: `${args[2]}/${args[0]}.task.json`, succ};
87
+ break;
88
+ };
89
+ case 'incrementRetry': ackValue = memoryStore.incrementRetry(args[0]); break;
90
+ case 'saveUserMessage': memoryStore.addMessage(args[0]); break;
91
+ case 'deleteUserMessage': memoryStore.clearMessages(args[0]); break;
92
+ default:
93
+ throw new Error(`unknown storage op "${op}"`);
94
+ };
95
+ }catch(e){
96
+ ok = false;
97
+ error = {message: e && e.message ? e.message : String(e)};
98
+ runLog.warn('[Master]: failed to apply storage op', op, e);
99
+ };
100
+ if(message.needAck){
101
+ try{
102
+ worker.send({__storageOpResult: true, ok, value: ackValue, error});
103
+ }catch(sendErr){
104
+ //来源 worker 已死: 回执发不出, 吞掉(在飞任务由崩溃处理兜底)
105
+ };
106
+ };
107
+ };
108
+
61
109
  let clearTaskTimer = (workerId)=>{
62
110
  let rec = workerTaskTimers[workerId];
63
111
  if(rec){
@@ -71,16 +119,58 @@ const start = (config)=>{
71
119
  clearTaskTimer(id);
72
120
  };
73
121
  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);
122
+ let writeCounterAndExit = ()=>{
123
+ //计数器行与正常完成路径一致: 始终进 run.log(progressBar 未接管终端时 echo 上屏);
124
+ //runLog 写流与 stdout 都是异步写, 先写完再延迟 exit, 防计数器行被 process.exit 截断
125
+ let finalStats = getStats();
126
+ runLog.write(`[Tasks] succeeded=${finalStats.succeeded}, failed=${finalStats.failed}, succ-but-save-failed=${finalStats.succButSaveFailed || 0}`);
127
+ progressCtl.finish();
128
+ setTimeout(()=>{
129
+ process.exit(0);
130
+ }, 10);
131
+ };
132
+ if(memoryStore){
133
+ //memory 档: 收尾排进派发同一串行队列(append-and-return, 绝不阻塞等队列——本函数在
134
+ //shouldTerminate 路径就是在队列任务内被调用的, 阻塞 await 队列即死锁): 已到达未应用的
135
+ //worker 存储操作先于收尾计数应用; 此后排队的已死 worker 操作按 terminate 语义丢弃
136
+ queue.enqueue(async ()=>{
137
+ writeCounterAndExit();
138
+ });
139
+ return;
140
+ };
141
+ writeCounterAndExit();
142
+ };
143
+ let finalizeTimeoutTaskInMemory = (subTask, cost)=>{
144
+ //memory 档: 与 file 档同语义同顺序(先结果后迁移); 只在 queue.enqueue 里调(与 worker 存储操作
145
+ //同一串行通道): 该 worker 已到达未应用的 saveResult 等操作必先于本判定应用, 已交卷任务不会被误杀
146
+ let subid = subTask.subid;
147
+ try{
148
+ let resultType = memoryStore.getSavedResultType(subid);
149
+ if(resultType === 'succ'){//超时边界: worker 已成功交卷, 视为已完成
150
+ memoryStore.move(subid, 'running', 'finished');
151
+ }else if(resultType === 'error'){//超时边界: worker 已自行记错误
152
+ memoryStore.move(subid, 'running', 'finished_with_errors');
153
+ }else{
154
+ if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
155
+ let retryCount = memoryStore.incrementRetry(subid);
156
+ if(retryCount <= config.maxTaskRetries){
157
+ runLog.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
158
+ memoryStore.move(subid, 'running', 'new');
159
+ return;
160
+ };
161
+ };
162
+ memoryStore.saveResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
163
+ memoryStore.move(subid, 'running', 'finished_with_errors');
164
+ }
165
+ }catch(e){
166
+ runLog.warn('[Master]: failed to finalize timeout task', subid, e);
167
+ }
82
168
  };
83
169
  let finalizeTimeoutTask = (subTask, cost)=>{
170
+ if(memoryStore){
171
+ queue.enqueue(async ()=>{ finalizeTimeoutTaskInMemory(subTask, cost); });
172
+ return;
173
+ };
84
174
  let subid = subTask.subid;
85
175
  try{
86
176
  let resultType = TaskMgr.getSavedResultType(subid);
@@ -114,8 +204,30 @@ const start = (config)=>{
114
204
  worker.kill();//不 disconnect: exit 事件会按崩溃补员
115
205
  finalizeTimeoutTask(subTask, cost);
116
206
  };
207
+ let handleCrashedTaskInMemory = (subTask)=>{
208
+ //memory 档: 与 file 档同语义(重试移回 new / 超限记 worker_crash 归档), 操作走内存存储;
209
+ //只在 queue.enqueue 里调: 该 worker 崩溃前已到达的存储操作必先应用, 再判定重试/归档
210
+ let subid = subTask.subid;
211
+ try{
212
+ let retryCount = memoryStore.incrementRetry(subid);
213
+ if(retryCount <= config.maxTaskRetries){
214
+ runLog.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
215
+ memoryStore.move(subid, 'running', 'new');
216
+ }else{
217
+ runLog.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
218
+ memoryStore.saveResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
219
+ memoryStore.move(subid, 'running', 'finished_with_errors');
220
+ };
221
+ }catch(e){
222
+ runLog.warn('[Master]: failed to handle crashed task', subid, e);
223
+ }
224
+ };
117
225
  let handleCrashedTask = (subTask)=>{
118
226
  if(!(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0)) return;
227
+ if(memoryStore){
228
+ queue.enqueue(async ()=>{ handleCrashedTaskInMemory(subTask); });
229
+ return;
230
+ };
119
231
  let subid = subTask.subid;
120
232
  try{
121
233
  let retryCount = TaskMgr.incrementTaskRetry(subid);
@@ -177,25 +289,36 @@ const start = (config)=>{
177
289
  }
178
290
  return;
179
291
  }
292
+ if(memoryStore && message && message.__storageOp){//memory 档 worker 存储操作: 与派发同一串行通道逐条应用, 禁止捎带合批
293
+ queue.enqueue(async ()=>{ applyStorageOp(worker, message); });
294
+ return;
295
+ }
180
296
  if(message === MSG_REQUEST_NEW_TASK){
181
297
  clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
182
298
  progressCtl.taskCompleted(worker.id);//worker 行回到 idle; 完成数按盘上结果目录统计, 不在此计数
183
299
  queue.enqueue(async () => {
184
- //use queue to make sure no I/O conflict; update/pop/判空须在同一个串行任务里原子完成
185
- await TaskMgr.updateNewTasks();
186
- let subTask = await TaskMgr.popNewTask();
187
- if(!subTask){
188
- //判空前强制绕开缓存重读一次磁盘再 pop: 防其他 worker 动态建任务(createNewTasks)
189
- //落在 update 与判空之间被漏判, 导致 autoClose 提前杀光 worker / 丢新任务
190
- await TaskMgr.loadNewTasks();
191
- subTask = await TaskMgr.popNewTask();
192
- }
193
- if(!subTask && config.autoCloseAfterCompletion){
194
- //杀 worker 前延时 1s 再兜底确认一次: 等其他 worker 可能正在落盘的动态新任务
195
- //(best-effort 再压小竞态窗口, 只限 autoClose 路径, 普通派发零开销)
196
- await new Promise((resolve)=>{ setTimeout(resolve, 1000); });
197
- await TaskMgr.loadNewTasks();
300
+ let subTask = null;
301
+ if(memoryStore){
302
+ //memory 档: 任务全在内存队列, pop 即出队; 无需磁盘重读兜底(动态任务经 __storageOp
303
+ //同一串行通道进来, pop 时必然已可见)
304
+ subTask = memoryStore.pop();
305
+ }else{
306
+ //use queue to make sure no I/O conflict; update/pop/判空须在同一个串行任务里原子完成
307
+ await TaskMgr.updateNewTasks();
198
308
  subTask = await TaskMgr.popNewTask();
309
+ if(!subTask){
310
+ //判空前强制绕开缓存重读一次磁盘再 pop: 防其他 worker 动态建任务(createNewTasks)
311
+ //落在 update 与判空之间被漏判, 导致 autoClose 提前杀光 worker / 丢新任务
312
+ await TaskMgr.loadNewTasks();
313
+ subTask = await TaskMgr.popNewTask();
314
+ }
315
+ if(!subTask && config.autoCloseAfterCompletion){
316
+ //杀 worker 前延时 1s 再兜底确认一次: 等其他 worker 可能正在写盘的动态新任务
317
+ //(best-effort 再压小竞态窗口, 只限 autoClose 路径, 普通派发零开销)
318
+ await new Promise((resolve)=>{ setTimeout(resolve, 1000); });
319
+ await TaskMgr.loadNewTasks();
320
+ subTask = await TaskMgr.popNewTask();
321
+ }
199
322
  }
200
323
  if(!subTask){
201
324
  //no tasks left
@@ -224,7 +347,7 @@ const start = (config)=>{
224
347
  //send new tasks to the worker
225
348
  let startTimestamp = new Date()*1;
226
349
  //IPC 用裁剪副本: worker 端只取目录路径, 不需要 initialTasks 全量数组;
227
- //带着它每次派发都要 O(N) 序列化(总量 O(N²))。config 本体保留: resume/restart 重写快照要用
350
+ //带着它每次派发都要 O(N) 序列化(总量 O(N²))。config 本体保留: resume/restart 重写存档要用
228
351
  let ipcConfig = { ...config };
229
352
  delete ipcConfig.initialTasks;
230
353
  worker.send({
@@ -250,8 +373,9 @@ const start = (config)=>{
250
373
  };
251
374
 
252
375
  let initPromise = config.__resume ? asMaster.resume(config) : asMaster.init(config);
253
- initPromise.then((updated_config)=>{
254
- config = updated_config;
376
+ initPromise.then((inited)=>{
377
+ config = inited.config;
378
+ memoryStore = inited.store || null;
255
379
  //一次性启动块(走 console, 在劫持前上屏): 标题框横幅 + 平铺关键字段, 模式由 __ 标记推导;
256
380
  //init 成功后所有信息齐备(new tasks 数/masterTaskId/__sysLogFile), 一次打完
257
381
  let startupInfo = {
@@ -261,7 +385,7 @@ const start = (config)=>{
261
385
  numOfWorkers,
262
386
  numCPUs,
263
387
  pid: process.pid,
264
- newTasks: TaskMgr.getTasks('new').length,
388
+ newTasks: getRemaining(),
265
389
  sysLogFile: config.__sysLogFile,
266
390
  startedAt: randomsUtil.getDateTimeTxt(),
267
391
  };
@@ -269,8 +393,8 @@ const start = (config)=>{
269
393
  progressCtl.setup({//TTY 判定 + 渲染器创建都在控制器内; 统计口径注入, 控制器不依赖 TaskMgr
270
394
  config,
271
395
  stream: process.stdout,
272
- getStats: () => TaskMgr.getFinalStats(),
273
- getRemaining: () => TaskMgr.getTasks('new').length,
396
+ getStats,
397
+ getRemaining,
274
398
  });
275
399
  //console 劫持写 run.log 是永久机制, 与 progressBar 无关: 进度区接管终端(开且 TTY)时只写文件,
276
400
  //否则 echo 同时透传 console 上屏; 判定收敛在 initAndHook 内, 无 __sysLogFile 时维持 console
@@ -310,12 +434,13 @@ const start = (config)=>{
310
434
  allkilled = allkilled && worker.killed;
311
435
  }
312
436
  if(allkilled){
437
+ let finishAll = ()=>{
313
438
  if(USER_CONFIG.onFinish){
314
439
  let endTimestamp = new Date()*1;
315
- let stats = TaskMgr.getFinalStats();
440
+ let stats = getStats();
316
441
  let report = {
317
442
  taskId: config.masterTaskId,
318
- taskFolder: stats.taskFolder,
443
+ taskFolder: config.taskFolder,
319
444
  startTimestamp: masterStartTimestamp,
320
445
  endTimestamp,
321
446
  cost: endTimestamp - masterStartTimestamp,
@@ -326,23 +451,43 @@ const start = (config)=>{
326
451
  succButSaveFailed: stats.succButSaveFailed || 0,//任务成功但结果保存失败(序列化兜底)的任务数, 明细见 .sys/user_messages
327
452
  workers: {count: numOfWorkers, crashed: workerCrashCount},
328
453
  };
329
- let onFinishHelper = {
330
- foreachSuccResult: (cb) => TaskMgr.forEachSuccResult(cb),
331
- foreachErrorResult: (cb) => TaskMgr.forEachErrorResult(cb),
332
- foreachResult: (cb) => TaskMgr.forEachResult(cb),
333
- };
454
+ let onFinishHelper;
455
+ if(config.task_storage === 'memory'){
456
+ //memory 档: 结果只在内存(盘上 results/ 恒空), 遍历接口委托 memoryStore(与 TaskMgr 同签名同口径)
457
+ onFinishHelper = {
458
+ foreachSuccResult: (cb) => memoryStore.forEachSuccResult(cb),
459
+ foreachErrorResult: (cb) => memoryStore.forEachErrorResult(cb),
460
+ foreachResult: (cb) => memoryStore.forEachResult(cb),
461
+ };
462
+ }else{
463
+ onFinishHelper = {
464
+ foreachSuccResult: (cb) => TaskMgr.forEachSuccResult(cb),
465
+ foreachErrorResult: (cb) => TaskMgr.forEachErrorResult(cb),
466
+ foreachResult: (cb) => TaskMgr.forEachResult(cb),
467
+ };
468
+ }
334
469
  USER_CONFIG.onFinish(report, onFinishHelper);
335
470
  delete USER_CONFIG.onFinish;
336
471
  }
337
472
  //计数器行先进 run.log(progressBar 未接管终端时 echo 上屏, 接管时由 progressCtl.finish() 打在屏幕);
338
473
  //stdout/run.log 都是异步写, 写完留 10ms 再 exit, 防 process.exit 截断未 flush 的输出
339
- let finalStats = TaskMgr.getFinalStats();
474
+ let finalStats = getStats();
340
475
  runLog.write(`[Tasks] succeeded=${finalStats.succeeded}, failed=${finalStats.failed}, succ-but-save-failed=${finalStats.succButSaveFailed || 0}`);
341
476
  runLog.write('[Master]: all workers are done, process exit.');
342
477
  progressCtl.finish();//单行进度条换行定格 + 计数器行, 别让 shell 提示符盖住它
343
478
  setTimeout(()=>{
344
479
  process.exit(0);
345
480
  }, 10);
481
+ };
482
+ if(memoryStore){
483
+ //memory 档: 同 terminateAllAndExit 口径, 收尾排进派发串行队列, 队列里已到达的存储操作
484
+ //先应用再统计; file 档维持原同步收尾一字不动
485
+ queue.enqueue(async ()=>{
486
+ finishAll();
487
+ });
488
+ }else{
489
+ finishAll();
490
+ }
346
491
  }
347
492
  });
348
493
  }else{
@@ -371,6 +516,10 @@ function subWorker(config){
371
516
  helper.__dispatchBroadcast(message);
372
517
  return;
373
518
  }
519
+ if(message && message.__storageOpResult){//memory 档存储操作回执: 兑付 IpcTaskStore 的 needAck 等待队列
520
+ IpcTaskStore.__dispatchStorageOpResult(message);
521
+ return;
522
+ }
374
523
  if(message === MSG_NO_TASK_FOUND){//wait for new task
375
524
  runLog.write(` (Worker #${worker.id}): waiting`);
376
525
  setTimeout(()=>{
@@ -379,7 +528,10 @@ function subWorker(config){
379
528
  }else
380
529
  if(message.subTask){//new task
381
530
  let {config, subTask, startTimestamp, taskCount} = message;
382
- TaskMgr.load(config);
531
+ //memory 档 worker 不碰任务目录: 存储后端换 IpcTaskStore(逐条 {__storageOp} 发 master),
532
+ //操作序列与 file 档逐条同构; file 档后端即 TaskMgr(磁盘, 现状不变)
533
+ let store = config.task_storage === 'memory' ? IpcTaskStore : TaskMgr;
534
+ store.load(config);
383
535
  //用户任务自带的 index/taskCount 保留原值(重试/续跑后任务身份不变),
384
536
  //未提供时才填全局派发计数器(v1 兼容)
385
537
  if(typeof subTask.index === 'undefined') subTask.index = taskCount;
@@ -393,7 +545,7 @@ function subWorker(config){
393
545
  taskCount,
394
546
  startTimestamp
395
547
  };
396
- runTask(config, subTask, subTaskLog).then((tasklog)=>{
548
+ runTask(config, subTask, subTaskLog, store).then((tasklog)=>{
397
549
  if(tasklog.retried) return;//重试任务已回 new 等重跑, 未完成, 不打 [Task Finished]
398
550
  let {cost, startTimestamp, endTimestamp} = tasklog;
399
551
 
@@ -425,13 +577,13 @@ function serializeError(e){
425
577
  };
426
578
  return serialized;
427
579
  };
428
- function runTask(config, subTask, subTaskLog){
580
+ function runTask(config, subTask, subTaskLog, store){
429
581
  return new Promise((resolve)=>{
430
582
  let resultPromise = Promise.resolve({});
431
583
  let resultData;
432
584
  let errors = [];
433
585
  helper.load(config);
434
- TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
586
+ store.saveTaskLog(subTask.subid, subTaskLog);
435
587
  try{
436
588
  resultPromise = USER_CONFIG.processTask(subTask, helper);//rewrite this method in subclass
437
589
  }catch(e){
@@ -456,21 +608,23 @@ function runTask(config, subTask, subTaskLog){
456
608
  if(hasError)runLog.warn('[Task Error]', subTask.subid, errors);
457
609
  let retried = false;
458
610
  if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
459
- let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
611
+ //重试决策在 worker(file 档如此, memory 档同构勿收归 master); await 两档通用:
612
+ //file 档 incrementTaskRetry 是同步值, memory 档是 needAck 回执 Promise——worker 侧唯一 async 化点
613
+ let retryCount = await store.incrementTaskRetry(subTask.subid);
460
614
  if(retryCount <= config.maxTaskRetries){
461
615
  runLog.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
462
616
  retried = true;
463
- //等 rename 落盘再 resolve/请求新任务: fire-and-forget 的异步 rename 会随 worker 被
464
- //autoClose 杀掉而消亡, 任务文件滞留 running 被静默丢弃(高负载偶发丢任务的根因, 勿回退)
617
+ //等 rename 生效再 resolve/请求新任务: file 档等 rename 写盘完成(fire-and-forget 的异步 rename 会随
618
+ //worker 被 autoClose 杀掉而消亡), memory 档等价等 master 回执——约定两档继续成立, 勿回退
465
619
  try{
466
- await TaskMgr.mvTaskPromise(subTask.subid, 'running', 'new');
620
+ await store.mvTaskPromise(subTask.subid, 'running', 'new');
467
621
  }catch(newPath){
468
622
  runLog.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
469
623
  };
470
- TaskMgr.deleteUserMessage(subTask.subid);//重跑即新一轮: 清掉该任务前序尝试的遗留消息
624
+ store.deleteUserMessage(subTask.subid);//重跑即新一轮: 清掉该任务前序尝试的遗留消息
471
625
  };
472
626
  };
473
- TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
627
+ store.saveTaskLog(subTask.subid, subTaskLog);
474
628
  if(retried){
475
629
  resolve({
476
630
  startTimestamp,
@@ -481,10 +635,10 @@ function runTask(config, subTask, subTaskLog){
481
635
  return;
482
636
  };
483
637
  if(!hasError){
484
- TaskMgr.deleteSavedResult(subTask.subid);//清掉前序尝试可能残留的(超时竞争)错误结果
638
+ store.deleteSavedResult(subTask.subid);//清掉前序尝试可能残留的(超时竞争)错误结果
485
639
  };
486
- TaskMgr.saveTaskResult(subTask.subid, hasError, hasError ? errors : resultData);
487
- TaskMgr.mvTask(subTask.subid, 'running',(hasError ? 'finished_with_errors':'finished'), (newPath)=>{
640
+ store.saveTaskResult(subTask.subid, hasError, hasError ? errors : resultData);
641
+ store.mvTask(subTask.subid, 'running',(hasError ? 'finished_with_errors':'finished'), (newPath)=>{
488
642
  resolve({
489
643
  startTimestamp,
490
644
  endTimestamp,