multi-tasks 3.0.2 → 3.0.4

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 CHANGED
@@ -1,164 +1,206 @@
1
- # multi-tasks
2
-
3
- Multi-tasks is a toolkit to manage long-term and large-scale parallel computing tasks. It manages progress and tasks based on the file system, which means that even if the host crashes, tasks can be resumed based on file records.
4
-
5
- **Zero dependencies:** multi-tasks has no runtime dependencies at all — it is built entirely on Node.js built-in modules (`cluster`, `fs`, `path`, `os`), so installing it adds nothing extra to your `node_modules`.
6
-
7
- ### Install:
8
-
9
- ```javascript
10
- npm install multi-tasks
11
- ```
12
-
13
- ### How to use:
14
-
15
- ```javascript
16
- //see examples/example0
17
- let multiTasks = require('multi-tasks').multiTasks;
18
-
19
- //Step1, create your tasks that need to be executed simultaneously as an array.
20
- let alltasks = [];
21
- for(let i=0;i<50;i++){
22
- alltasks.push({
23
- name: `task-${i}`,
24
- data: `This prop is for a subtask`
25
- });
26
- };
27
-
28
- //Step2, provide a function to process a certain sub-task and return the result data
29
- let processTask = (task, helper)=>{
30
- let {taskCount} = task;//get your task data
31
-
32
- //Run your processing logic here...
33
-
34
- //then return the result as a plain javascript object or an async promise
35
- return 'result data';
36
- };
37
-
38
- //Step3, run!
39
- multiTasks({
40
- initialTasks: alltasks,
41
- processTask,
42
- taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and results files, you can check the progress here
43
- taskId: 'my-task',
44
- numberOfWorkers: 3, //how many workers are working in parallel
45
- //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
46
- //autoCloseAfterCompletion: true, //if you have dynamically generated new tasks, put this as false
47
- shouldTerminate:(info)=>{
48
- //return true if you need to terminate the whole process
49
- },
50
- onFinish: (report)=>{
51
- console.log('finish callback', report);
52
- }
53
- });
54
-
55
- ```
56
-
57
- ### More about the task processing function
58
-
59
- ```javascript
60
- //Example1, return a promise for async processes,
61
- // you can return a promise or a non-promise result,
62
- // all result data can be found in the results/succ folder
63
- let processTask = (task, helper)=>{
64
- let {taskCount} = task;
65
-
66
- return new Promise((resolve, reject)=>{
67
- resolve({
68
- data:`task${taskCount} complete`
69
- })
70
- })
71
- };
72
-
73
- //Example2, dynamically create a new task while processing
74
- let processTask = (task, helper)=>{
75
- let {taskCount} = task;
76
-
77
- if(taskCount % 2 === 0){
78
- //create a new task if needed
79
- helper.createNewTasks({
80
- msg:'a new task'
81
- });
82
- return;
83
- }
84
-
85
- return new Promise((resolve, reject)=>{
86
- resolve({
87
- data:`task${taskCount} complete`
88
- })
89
- })
90
- };
91
-
92
- //Example3, generate/throw exceptions in a task method
93
- let processTask = (task, helper)=>{
94
- let {taskCount} = task;
95
-
96
- //This is the demo of exceptions/errors, they will be captured and saved in the results/errors folder
97
- if(taskCount===3) throw 'exception';
98
- if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
99
- if(taskCount===5) aaa = bbb;//this undefined exception will be captured by multi-tasks
100
-
101
- return {data:'succ'};
102
- }
103
-
104
-
105
- ```
106
-
107
- ### Resuming
108
-
109
- Sometimes the task execution is interrupted due to some reasons (such as power outage), you can resume the execution like this
110
-
111
- ```javascript
112
-
113
- multiTasks({
114
- initialTasks: `/myworks/my_scan_tasks/`, //Point 'initialTasks' to the interrupted task directory, multi-tasks will read the tasks in the 'new' folder and initialize them to 'initialTasks' and then continue execution
115
- ...
116
- ...//Other configurations remain unchanged
117
- ...
118
- });
119
-
120
- ```
121
-
122
- ### Testing:
123
-
124
- The usage patterns documented above are covered by automated tests (unit tests in `test/`, end-to-end tests in `teste2e/`). The e2e suite runs the API in real child processes with a mixed workload — successful tasks (both promise and non-promise results), planned failures (rejected promises and thrown exceptions), dynamically created tasks via `helper.createNewTasks`, and workers that crash randomly — then resumes repeatedly and verifies that every task lands exactly one result in `results/succ` or `results/errors`.
125
-
126
- ### Changelog:
127
-
128
- - 3.0.2 Support taskTimeout: a task that does not finish in time is treated as failed with a timeout error
129
- - 3.0.1 Versions 3.0.0 and above are maintained by AI
130
- - 2.2.0 Support Resuming from an interrupted task
131
- - 2.1.2 Update readme
132
- - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
133
- - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
134
- - 2.0.9 Update readme examples
135
- - 2.0.8 Update readme examples
136
- - 2.0.7 Update changelog
137
- - 2.0.6 Update readme, remove failed examples
138
- - 2.0.5 Update readme examples
139
- - 2.0.4 Support resume from a failed task
140
- - 2.0.3 Fix: mkdir bug on windows
141
- - 2.0.2 new feature: support shouldTerminate
142
- - 2.0.1 Avoid possible I/O conflicts.
143
- - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
144
- - 1.2.8 Fix: create task folder failed on MacOS
145
- - 1.2.7 Small updates
146
- - 1.2.6 Support onFinish event
147
- - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
148
- - 1.2.4 Fix: opt.numberOfWorkers not work
149
- - 1.2.3 Update README
150
- - 1.2.2 Update README and examples
151
- - 1.2.1 Handle exceptions and errors in subtasks
152
- - 1.2.0 Simplified usage by providing the function way and support return Promise
153
- - 1.1.4 Remove make-dir
154
- - 1.1.3 Simplified usage, see example0
155
- - 1.1.2
156
- - 1.1.1 Rename files, updated changelog
157
- - 1.1.0 Simplified the usage of a customized Consumer, see example0
158
- - 1.0.8 Fix examples
159
- - 1.0.7 Remove moment
160
- - 1.0.6 Performance optimization
161
-
162
- ### Gitee:
163
-
164
- [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
1
+ # multi-tasks
2
+
3
+ Multi-tasks is a toolkit to manage long-term and large-scale parallel computing tasks. It manages progress and tasks based on the file system, which means that even if the host crashes, tasks can be resumed based on file records.
4
+
5
+ **Zero dependencies:** multi-tasks has no runtime dependencies at all — it is built entirely on Node.js built-in modules (`cluster`, `fs`, `path`, `os`), so installing it adds nothing extra to your `node_modules`.
6
+
7
+ ### Install:
8
+
9
+ ```javascript
10
+ npm install multi-tasks
11
+ ```
12
+
13
+ ### API:
14
+
15
+ - `multiTasks(config)` — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
16
+ - `multiTasks.start(config)` — always start a fresh run.
17
+ - `multiTasks.resume(config)` resume an interrupted run.
18
+ - `multiTasks.restart(config)` — resume and also retry failed tasks (needs `config.taskFolder`).
19
+ - `helper.createNewTasks(tasks)` create new tasks dynamically while processing.
20
+ - Config options:
21
+ - `initialTasks` — array of task objects, or a folder path string to resume from.
22
+ - `processTask(task, helper)` — required; return a value or a Promise.
23
+ - `taskRootFolder` — root directory where progress and result files are stored.
24
+ - `taskId` task folder name under `taskRootFolder`.
25
+ - `taskFolder` — full task folder path, required by `restart`.
26
+ - `numberOfWorkers` — how many worker processes run in parallel; also accepts a percentage string of CPU cores, e.g. `"50%"`.
27
+ - `taskTimeout` — optional, milliseconds; an overdue task fails with a timeout error.
28
+ - `maxTaskRetries` optional, max times a failed task is auto-retried.
29
+ - `autoCloseAfterCompletion` set `false` if you create new tasks dynamically.
30
+ - `shouldTerminate(info)` return `true` to terminate the whole process.
31
+ - `onFinish(report)` — called once after all workers are done.
32
+
33
+ ### How to use:
34
+
35
+ ```javascript
36
+ //see examples/example0
37
+ let multiTasks = require('multi-tasks').multiTasks;
38
+
39
+ //Step1, create your tasks that need to be executed simultaneously as an array.
40
+ let alltasks = [];
41
+ for(let i=0;i<50;i++){
42
+ alltasks.push({
43
+ name: `task-${i}`,
44
+ data: `This prop is for a subtask`
45
+ });
46
+ };
47
+
48
+ //Step2, provide a function to process a certain sub-task and return the result data
49
+ let processTask = (task, helper)=>{
50
+ let {taskCount} = task;//get your task data
51
+
52
+ //Run your processing logic here...
53
+
54
+ //then return the result as a plain javascript object or an async promise
55
+ return 'result data';
56
+ };
57
+
58
+ //Step3, run!
59
+ multiTasks({
60
+ initialTasks: alltasks,
61
+ processTask,
62
+ taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and results files, you can check the progress here
63
+ taskId: 'my-task',
64
+ numberOfWorkers: 3, //how many workers are working in parallel
65
+ //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
66
+ //maxTaskRetries: 2, //optional, auto-retry a failed task (worker crash, processTask error, or timeout); retried tasks go back to the queue
67
+ //autoCloseAfterCompletion: true, //if you have dynamically generated new tasks, put this as false
68
+ shouldTerminate:(info)=>{
69
+ //return true if you need to terminate the whole process
70
+ },
71
+ onFinish: (report)=>{
72
+ console.log('finish callback', report);
73
+ }
74
+ });
75
+
76
+ ```
77
+
78
+ ### More about the task processing function
79
+
80
+ ```javascript
81
+ //Example1, return a promise for async processes,
82
+ // you can return a promise or a non-promise result,
83
+ // all result data can be found in the results/succ folder
84
+ let processTask = (task, helper)=>{
85
+ let {taskCount} = task;
86
+
87
+ return new Promise((resolve, reject)=>{
88
+ resolve({
89
+ data:`task${taskCount} complete`
90
+ })
91
+ })
92
+ };
93
+
94
+ //Example2, dynamically create a new task while processing
95
+ let processTask = (task, helper)=>{
96
+ let {taskCount} = task;
97
+
98
+ if(taskCount % 2 === 0){
99
+ //create a new task if needed
100
+ helper.createNewTasks({
101
+ msg:'a new task'
102
+ });
103
+ return;
104
+ }
105
+
106
+ return new Promise((resolve, reject)=>{
107
+ resolve({
108
+ data:`task${taskCount} complete`
109
+ })
110
+ })
111
+ };
112
+
113
+ //Example3, generate/throw exceptions in a task method
114
+ let processTask = (task, helper)=>{
115
+ let {taskCount} = task;
116
+
117
+ //This is the demo of exceptions/errors, they will be captured and saved in the results/errors folder
118
+ if(taskCount===3) throw 'exception';
119
+ if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
120
+ if(taskCount===5) aaa = bbb;//this undefined exception will be captured by multi-tasks
121
+
122
+ return {data:'succ'};
123
+ }
124
+
125
+ //Example5, auto-retry a failed task:
126
+ // when a task fails - its worker process crashes, processTask throws
127
+ // or rejects, or it exceeds taskTimeout - it is sent back to the
128
+ // queue and retried, at most maxTaskRetries times; when the limit
129
+ // is reached it lands in finished_with_errors (crash:
130
+ // {type:'worker_crash'}, timeout: {type:'timeout'}, processTask
131
+ // error: the original error data)
132
+ multiTasks({
133
+ initialTasks: alltasks,
134
+ processTask,
135
+ taskRootFolder: `../examples-tmp-data/example-retry`,
136
+ taskId: 'my-task',
137
+ numberOfWorkers: 3,
138
+ maxTaskRetries: 2,
139
+ });
140
+
141
+ ```
142
+
143
+ ### Resuming
144
+
145
+ Sometimes the task execution is interrupted due to some reasons (such as power outage), you can resume the execution like this
146
+
147
+ ```javascript
148
+
149
+ multiTasks({
150
+ initialTasks: `/myworks/my_scan_tasks/`, //Point 'initialTasks' to the interrupted task directory, multi-tasks will read the tasks in the 'new' folder and initialize them to 'initialTasks' and then continue execution
151
+ ...
152
+ ...//Other configurations remain unchanged
153
+ ...
154
+ });
155
+
156
+ ```
157
+
158
+ ### Testing:
159
+
160
+ The usage patterns documented above are covered by automated tests (unit tests in `test/`, end-to-end tests in `teste2e/`). The e2e suite runs the API in real child processes with a mixed workload — successful tasks (both promise and non-promise results), planned failures (rejected promises and thrown exceptions), dynamically created tasks via `helper.createNewTasks`, and workers that crash randomly — then resumes repeatedly and verifies that every task lands exactly one result in `results/succ` or `results/errors`.
161
+
162
+ ### Changelog:
163
+
164
+ - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
165
+ - 3.0.3 Support 'maxTaskRetries'
166
+ - 3.0.2 Support 'taskTimeout'
167
+ - 3.0.1 Versions 3.0.0 and above are maintained by AI
168
+ - 2.2.0 Support Resuming from an interrupted task
169
+ - 2.1.2 Update readme
170
+ - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
171
+ - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
172
+ - 2.0.9 Update readme examples
173
+ - 2.0.8 Update readme examples
174
+ - 2.0.7 Update changelog
175
+ - 2.0.6 Update readme, remove failed examples
176
+ - 2.0.5 Update readme examples
177
+ - 2.0.4 Support resume from a failed task
178
+ - 2.0.3 Fix: mkdir bug on windows
179
+ - 2.0.2 new feature: support shouldTerminate
180
+ - 2.0.1 Avoid possible I/O conflicts.
181
+ - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
182
+ - 1.2.8 Fix: create task folder failed on MacOS
183
+ - 1.2.7 Small updates
184
+ - 1.2.6 Support onFinish event
185
+ - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
186
+ - 1.2.4 Fix: opt.numberOfWorkers not work
187
+ - 1.2.3 Update README
188
+ - 1.2.2 Update README and examples
189
+ - 1.2.1 Handle exceptions and errors in subtasks
190
+ - 1.2.0 Simplified usage by providing the function way and support return Promise
191
+ - 1.1.4 Remove make-dir
192
+ - 1.1.3 Simplified usage, see example0
193
+ - 1.1.2
194
+ - 1.1.1 Rename files, updated changelog
195
+ - 1.1.0 Simplified the usage of a customized Consumer, see example0
196
+ - 1.0.8 Fix examples
197
+ - 1.0.7 Remove moment
198
+ - 1.0.6 Performance optimization
199
+
200
+ ### Gitee:
201
+
202
+ [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
203
+
204
+ ### License:
205
+
206
+ [MIT](https://opensource.org/licenses/MIT) (see [LICENSE](LICENSE))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
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": [
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const numCPUs = require('os').cpus().length;
4
+
5
+ const PERCENT_PATTERN = /^(\d+(?:\.\d+)?)%$/;
6
+
7
+ // 解析 numberOfWorkers 配置:
8
+ // - undefined -> CPU 核数减 1(默认);
9
+ // - number -> 原样返回(现状行为不变);
10
+ // - string -> 必须是 "数字%" 形式且百分比在 1-100 之间, 按核数四舍五入折算, 最小为 1;
11
+ // - 其余 -> 抛 Error。
12
+ const resolveNumberOfWorkers = (value) => {
13
+ if (typeof value === 'undefined') {
14
+ return numCPUs - 1;
15
+ };
16
+ if (typeof value === 'number') {
17
+ return value;
18
+ };
19
+ if (typeof value === 'string') {
20
+ const match = value.trim().match(PERCENT_PATTERN);
21
+ if (match) {
22
+ const percent = parseFloat(match[1]);
23
+ if (percent >= 1 && percent <= 100) {
24
+ return Math.max(1, Math.round(numCPUs * percent / 100));
25
+ };
26
+ };
27
+ };
28
+ throw new Error(`Invalid numberOfWorkers: ${JSON.stringify(value)}. Expected a number or a percentage string like "50%" (1-100).`);
29
+ };
30
+
31
+ module.exports = { resolveNumberOfWorkers };
@@ -8,6 +8,7 @@ const asMaster = require('./asMaster');
8
8
  const TaskMgr = require('./TaskMgr');
9
9
  const WorkerMgr = require('./WorkerMgr');
10
10
  const helper = require('./helper');
11
+ const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
11
12
 
12
13
  let fmtdigit = (n)=>{
13
14
  return n > 9 ? "" + n: "0" + n;
@@ -50,6 +51,7 @@ const start = (config)=>{
50
51
  if (isMaster) {//init
51
52
  console.log('[Master]: Start!!');
52
53
  let PID = process.pid;
54
+ config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串; 须在 fork 循环读值前折算, 非法值在此抛错
53
55
  let numOfWorkers = config.numberOfWorkers;
54
56
  console.log(` numCPUs=${numCPUs}`);
55
57
  console.log(` numOfWorkers=${numOfWorkers}`);
@@ -74,7 +76,15 @@ const start = (config)=>{
74
76
  }else if(resultType === 'error'){//超时边界: worker 已自行记错误
75
77
  TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
76
78
  }else{
77
- TaskMgr.saveTaskResult(subid, true, [{type: '超时', timeout: config.taskTimeout, cost}]);
79
+ if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
80
+ let retryCount = TaskMgr.incrementTaskRetry(subid);
81
+ if(retryCount <= config.maxTaskRetries){
82
+ console.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
83
+ TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
84
+ return;
85
+ };
86
+ };
87
+ TaskMgr.saveTaskResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
78
88
  TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
79
89
  }
80
90
  }catch(e){
@@ -91,6 +101,23 @@ const start = (config)=>{
91
101
  worker.kill();//不 disconnect: exit 事件会按崩溃补员
92
102
  finalizeTimeoutTask(subTask, cost);
93
103
  };
104
+ let handleCrashedTask = (subTask)=>{
105
+ if(!(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0)) return;
106
+ let subid = subTask.subid;
107
+ try{
108
+ let retryCount = TaskMgr.incrementTaskRetry(subid);
109
+ if(retryCount <= config.maxTaskRetries){
110
+ console.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
111
+ TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
112
+ }else{
113
+ console.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
114
+ TaskMgr.saveTaskResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
115
+ TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
116
+ };
117
+ }catch(e){
118
+ console.warn('[Master]: failed to handle crashed task', subid, e);
119
+ }
120
+ };
94
121
 
95
122
  let initWorker = (worker)=>{
96
123
  WorkerMgr.addWorker(worker);
@@ -128,9 +155,11 @@ const start = (config)=>{
128
155
  },()=>{}, {
129
156
  keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
130
157
  });
131
- if(typeof config.taskTimeout === 'number' && config.taskTimeout > 0){
158
+ let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
159
+ let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
160
+ if(hasTimeout || hasRetry){
132
161
  workerTaskTimers[worker.id] = {
133
- timer: setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout),
162
+ timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
134
163
  subTask,
135
164
  startTimestamp
136
165
  };
@@ -152,10 +181,14 @@ const start = (config)=>{
152
181
  initWorker(worker);
153
182
  }
154
183
  });
155
- cluster.on('exit', (worker, code, signal) => {
184
+ cluster.on('exit', (worker, code, signal) => {
185
+ let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
156
186
  clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
157
187
  if (code !== 0 && !worker.exitedAfterDisconnect) {
158
188
  console.log('Worker died unexpectedly starting a new one...');
189
+ if(rec && rec.subTask){
190
+ handleCrashedTask(rec.subTask);
191
+ };
159
192
  let worker = cluster.fork();
160
193
  initWorker(worker);
161
194
  return;
@@ -252,7 +285,27 @@ function runTask(config, subTask, subTaskLog){
252
285
  };
253
286
  let hasError = errors.length > 0;
254
287
  if(hasError)console.warn('[Task Error]', subTask.subid, errors);
288
+ let retried = false;
289
+ if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
290
+ let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
291
+ if(retryCount <= config.maxTaskRetries){
292
+ console.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
293
+ TaskMgr.mvTask(subTask.subid, 'running', 'new', ()=>{});
294
+ retried = true;
295
+ };
296
+ };
255
297
  TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
298
+ if(retried){
299
+ resolve({
300
+ startTimestamp,
301
+ endTimestamp,
302
+ cost
303
+ });
304
+ return;
305
+ };
306
+ if(!hasError){
307
+ TaskMgr.deleteSavedResult(subTask.subid);//清掉前序尝试可能残留的(超时竞争)错误结果
308
+ };
256
309
  TaskMgr.saveTaskResult(subTask.subid, hasError, hasError ? errors : resultData);
257
310
  TaskMgr.mvTask(subTask.subid, 'running',(hasError ? 'finished_with_errors':'finished'), (newPath)=>{
258
311
  resolve({
@@ -225,6 +225,30 @@ const TaskMgr = {
225
225
  fcontent = JSON.stringify(log);
226
226
  };
227
227
  fs.writeFileSync(fpath, fcontent);
228
- }
228
+ },
229
+ incrementTaskRetry:(subid)=>{
230
+ let {taskFolderRunning} = main_config;
231
+ let fpath = pathutil.resolve(taskFolderRunning, `./${subid}.task.json`);
232
+ let task = JSON.parse(fs.readFileSync(fpath, 'utf-8'));
233
+ task.__retryCount = (task.__retryCount || 0) + 1;
234
+ fs.writeFileSync(fpath, JSON.stringify(task));
235
+ return task.__retryCount;
236
+ },
237
+ deleteSavedResult:(subid)=>{
238
+ let {taskFolderResult, taskFolderResultFailed} = main_config;
239
+ [taskFolderResult, taskFolderResultFailed].forEach((folder)=>{
240
+ let fpath = pathutil.resolve(folder, `${subid}.json`);
241
+ if(fs.existsSync(fpath)){
242
+ fs.unlinkSync(fpath);
243
+ }
244
+ });
245
+ },
246
+ resetTaskRetry:(subid)=>{
247
+ let {taskFolderNew} = main_config;
248
+ let fpath = pathutil.resolve(taskFolderNew, `./${subid}.task.json`);
249
+ let task = JSON.parse(fs.readFileSync(fpath, 'utf-8'));
250
+ delete task.__retryCount;
251
+ fs.writeFileSync(fpath, JSON.stringify(task));
252
+ },
229
253
  };
230
254
  module.exports = TaskMgr;
@@ -1,9 +1,9 @@
1
1
  var fs = require('fs');
2
2
  var pathUtil = require('path');
3
- const numCPUs = require('os').cpus().length;
4
3
 
5
4
  const TaskMgr = require('./TaskMgr');
6
5
  const randomsUtil = require('../utils/randoms');
6
+ const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
7
7
 
8
8
  const resume = (config)=>{
9
9
  config.__resume = true;
@@ -32,7 +32,9 @@ const resume = (config)=>{
32
32
  if(config.__restart){
33
33
  errfiles.forEach((fname)=>{
34
34
  let subid = fname.replace(/\.task\.json$/, '');
35
- promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new'));
35
+ promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
36
+ TaskMgr.resetTaskRetry(subid);//restart 重跑失败任务时重置重试计数(resume 不重置)
37
+ }));
36
38
  });
37
39
  };
38
40
  Promise.all(promises).then(()=>{
@@ -58,7 +60,7 @@ const init = (config)=>{
58
60
  if(!config.initialTasks){ throw new Error('Please provide initialTasks data!'); };
59
61
  if(!config.processTask){ throw new Error('Please provide processTask function!'); };
60
62
 
61
- if(typeof config.numberOfWorkers === 'undefined') config.numberOfWorkers = numCPUs - 1;
63
+ config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
62
64
 
63
65
  let {taskRootFolder, taskName} = config;
64
66