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.
@@ -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 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;
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
- memoryStore.add(args[0]);
79
+ taskStore.add(args[0]);
80
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;
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 = memoryStore.move(args[0], args[1], args[2]);
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 = memoryStore.incrementRetry(args[0]); break;
90
- case 'saveUserMessage': memoryStore.addMessage(args[0]); break;
91
- case 'deleteUserMessage': memoryStore.clearMessages(args[0]); break;
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(memoryStore){
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 finalizeTimeoutTaskInMemory = (subTask, cost)=>{
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 = memoryStore.getSavedResultType(subid);
156
+ let resultType = taskStore.getSavedResultType(subid);
149
157
  if(resultType === 'succ'){//超时边界: worker 已成功交卷, 视为已完成
150
- memoryStore.move(subid, 'running', 'finished');
158
+ taskStore.move(subid, 'running', 'finished');
151
159
  }else if(resultType === 'error'){//超时边界: worker 已自行记错误
152
- memoryStore.move(subid, 'running', 'finished_with_errors');
160
+ taskStore.move(subid, 'running', 'finished_with_errors');
153
161
  }else{
154
162
  if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
155
- let retryCount = memoryStore.incrementRetry(subid);
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
- memoryStore.move(subid, 'running', 'new');
166
+ taskStore.move(subid, 'running', 'new');
159
167
  return;
160
168
  };
161
169
  };
162
- memoryStore.saveResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
163
- memoryStore.move(subid, 'running', 'finished_with_errors');
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(memoryStore){
171
- queue.enqueue(async ()=>{ finalizeTimeoutTaskInMemory(subTask, cost); });
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 handleCrashedTaskInMemory = (subTask)=>{
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 = memoryStore.incrementRetry(subid);
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
- memoryStore.move(subid, 'running', 'new');
223
+ taskStore.move(subid, 'running', 'new');
216
224
  }else{
217
225
  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');
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(memoryStore){
228
- queue.enqueue(async ()=>{ handleCrashedTaskInMemory(subTask); });
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(memoryStore && message && message.__storageOp){//memory 档 worker 存储操作: 与派发同一串行通道逐条应用, 禁止捎带合批
293
- queue.enqueue(async ()=>{ applyStorageOp(worker, message); });
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(memoryStore){
302
- //memory 档: 任务全在内存队列, pop 即出队; 无需磁盘重读兜底(动态任务经 __storageOp
303
- //同一串行通道进来, pop 时必然已可见)
304
- subTask = memoryStore.pop();
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
- memoryStore = inited.store || null;
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(config.task_storage === 'memory'){
456
- //memory 档: 结果只在内存(盘上 results/ 恒空), 遍历接口委托 memoryStore(与 TaskMgr 同签名同口径)
472
+ if(taskStore){
473
+ //memory/aof 档: 结果在 master 侧 store(aof 档收尾物化 results/ 文件, 内容一致), 遍历接口委托 taskStore(与 TaskMgr 同签名同口径)
457
474
  onFinishHelper = {
458
- foreachSuccResult: (cb) => memoryStore.forEachSuccResult(cb),
459
- foreachErrorResult: (cb) => memoryStore.forEachErrorResult(cb),
460
- foreachResult: (cb) => memoryStore.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
- USER_CONFIG.onFinish(report, onFinishHelper);
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(memoryStore){
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 === 'memory' ? IpcTaskStore : TaskMgr;
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
- let retryCount = await store.incrementTaskRetry(subTask.subid);
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;
@@ -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
- const {masterTaskId, taskFolderNew} = main_config;
109
- const subid = randomsUtil.get36Id();
110
-
111
- task.masterTaskId = masterTaskId;
112
- task.subid = `${masterTaskId}--${subid}`;
113
- let taskfilepath = pathutil.resolve(taskFolderNew, `./${task.subid}.task.json`);
114
- fs.writeFileSync(taskfilepath, JSON.stringify(task));
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;
@@ -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 randomsUtil = require('../utils/randoms');
7
- const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
8
-
9
- const resume = (config)=>{
10
- config.__resume = true;
11
- let {taskRootFolder, taskId} = config;
12
- let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
13
-
14
- //load from config file: 旧存档作底(兼作任务目录合法性校验, 不存在抛 ENOENT),
15
- //本次调用传入的配置覆盖同名字段——存档只补未传入的字段
16
- let task_config = JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json')));
17
- Object.keys(task_config).forEach((key)=>{
18
- if(key.startsWith('__')) delete task_config[key];//读侧剔除一次性标记, 兼容 <=3.1.3 旧存档
19
- });
20
- config = {
21
- ...task_config,
22
- ...config
23
- };
24
- config.masterTaskId = taskId;
25
-
26
- TaskMgr.load(config);
27
- config = TaskMgr.initFolders(config);
28
-
29
- return new Promise((resolve, reject)=>{
30
-
31
- TaskMgr.getTaskFiles('running', (taskfiles)=>{
32
- TaskMgr.getTaskFiles('finished_with_errors', (errfiles)=>{
33
- let promises = [];
34
- taskfiles.forEach((fname)=>{
35
- let subid = fname.replace(/\.task\.json$/, '');
36
- promises.push(TaskMgr.mvTaskPromise(subid, 'running', 'new'));
37
- });
38
- if(config.__retryFails){
39
- errfiles.forEach((fname)=>{
40
- let subid = fname.replace(/\.task\.json$/, '');
41
- promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
42
- TaskMgr.resetTaskRetry(subid);//retry_fails 重跑失败任务时重置重试计数(resume 不重置)
43
- TaskMgr.deleteUserMessage(subid);//重跑即新一轮: 清掉该任务在 .sys/user_messages 的遗留消息
44
- }));
45
- });
46
- };
47
- Promise.all(promises).then(()=>{
48
- TaskMgr.loadAllTasks().then((types)=>{
49
- console.log('******resume multi-thread')
50
- setTimeout(()=>{
51
- resolve({config, store: null});//memory 档 resume 在 index.js 已被拦截 throw, 到这里的都是 file 档
52
- }, 1000);
53
- });
54
-
55
- }).catch((e)=>{
56
- reject(e);
57
- });
58
- });
59
- });
60
- });
61
-
62
-
63
- };
64
-
65
- const init = (config)=>{
66
- if(!config) config = {};
67
- if(!config.initialTasks){ throw new Error('Please provide initialTasks data!'); };
68
- if(!config.processTask){ throw new Error('Please provide processTask function!'); };
69
-
70
- config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
71
-
72
- let {taskRootFolder, taskName} = config;
73
-
74
- if(taskName && !taskName.match(/^[a-zA-Z0-9_-]+$/)){ throw new Error('Error: taskName can only contain letters and numbers'); }
75
- if(!taskName){ taskName = 'multitasks'; }
76
-
77
- let {round, pow, random} = Math;
78
- let dateTimeText = randomsUtil.getDateTimeTxt();
79
- let randomNum = round((random() * pow(10,8)),8).toString(36);
80
- let masterTaskId = config.taskId ? config.taskId : `${taskName}${dateTimeText}--${randomNum}`;
81
-
82
- config.taskRootFolder = taskRootFolder;
83
- config.masterTaskId = masterTaskId;
84
-
85
-
86
- TaskMgr.load(config);
87
- config = TaskMgr.initFolders(config);
88
-
89
- if(config.task_storage === 'memory'){
90
- //memory 档: 不建任务文件(目录与 task_config.json 照建照写), 播种进 master 内存存储
91
- const store = createMemoryStore(config);
92
- store.addMany(config.initialTasks);
93
- return Promise.resolve({config, store});
94
- };
95
-
96
- config.initialTasks.forEach((task)=>{
97
- TaskMgr.createNewTask(task);
98
- });
99
-
100
- //to reload is for to reStart a collapsed task
101
- return new Promise((resolve)=>{
102
- TaskMgr.loadNewTasks().then(()=>{
103
- resolve({config, store: null});
104
- });
105
- });
106
- };
107
- module.exports = {
108
- init,
109
- resume
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
  };