multi-tasks 3.0.2 → 3.0.3

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,201 @@
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.
27
+ - `taskTimeout` — optional, milliseconds; an overdue task fails with a timeout error.
28
+ - `maxTaskRetries` — optional, max times a failed task is auto-retried (sent back to the queue immediately); covers worker crashes, exceptions/rejections from `processTask`, and `taskTimeout` timeouts; the retry count is kept in the task file as `__retryCount` — it survives `resume` and is reset by `restart`.
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.3 Support 'maxTaskRetries'
165
+ - 3.0.2 Support 'taskTimeout'
166
+ - 3.0.1 Versions 3.0.0 and above are maintained by AI
167
+ - 2.2.0 Support Resuming from an interrupted task
168
+ - 2.1.2 Update readme
169
+ - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
170
+ - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
171
+ - 2.0.9 Update readme examples
172
+ - 2.0.8 Update readme examples
173
+ - 2.0.7 Update changelog
174
+ - 2.0.6 Update readme, remove failed examples
175
+ - 2.0.5 Update readme examples
176
+ - 2.0.4 Support resume from a failed task
177
+ - 2.0.3 Fix: mkdir bug on windows
178
+ - 2.0.2 new feature: support shouldTerminate
179
+ - 2.0.1 Avoid possible I/O conflicts.
180
+ - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
181
+ - 1.2.8 Fix: create task folder failed on MacOS
182
+ - 1.2.7 Small updates
183
+ - 1.2.6 Support onFinish event
184
+ - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
185
+ - 1.2.4 Fix: opt.numberOfWorkers not work
186
+ - 1.2.3 Update README
187
+ - 1.2.2 Update README and examples
188
+ - 1.2.1 Handle exceptions and errors in subtasks
189
+ - 1.2.0 Simplified usage by providing the function way and support return Promise
190
+ - 1.1.4 Remove make-dir
191
+ - 1.1.3 Simplified usage, see example0
192
+ - 1.1.2
193
+ - 1.1.1 Rename files, updated changelog
194
+ - 1.1.0 Simplified the usage of a customized Consumer, see example0
195
+ - 1.0.8 Fix examples
196
+ - 1.0.7 Remove moment
197
+ - 1.0.6 Performance optimization
198
+
199
+ ### Gitee:
200
+
201
+ [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
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": [
@@ -74,7 +74,15 @@ const start = (config)=>{
74
74
  }else if(resultType === 'error'){//超时边界: worker 已自行记错误
75
75
  TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
76
76
  }else{
77
- TaskMgr.saveTaskResult(subid, true, [{type: '超时', timeout: config.taskTimeout, cost}]);
77
+ if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
78
+ let retryCount = TaskMgr.incrementTaskRetry(subid);
79
+ if(retryCount <= config.maxTaskRetries){
80
+ console.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
81
+ TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
82
+ return;
83
+ };
84
+ };
85
+ TaskMgr.saveTaskResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
78
86
  TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
79
87
  }
80
88
  }catch(e){
@@ -91,6 +99,23 @@ const start = (config)=>{
91
99
  worker.kill();//不 disconnect: exit 事件会按崩溃补员
92
100
  finalizeTimeoutTask(subTask, cost);
93
101
  };
102
+ let handleCrashedTask = (subTask)=>{
103
+ if(!(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0)) return;
104
+ let subid = subTask.subid;
105
+ try{
106
+ let retryCount = TaskMgr.incrementTaskRetry(subid);
107
+ if(retryCount <= config.maxTaskRetries){
108
+ console.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
109
+ TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
110
+ }else{
111
+ console.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
112
+ TaskMgr.saveTaskResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
113
+ TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
114
+ };
115
+ }catch(e){
116
+ console.warn('[Master]: failed to handle crashed task', subid, e);
117
+ }
118
+ };
94
119
 
95
120
  let initWorker = (worker)=>{
96
121
  WorkerMgr.addWorker(worker);
@@ -128,9 +153,11 @@ const start = (config)=>{
128
153
  },()=>{}, {
129
154
  keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
130
155
  });
131
- if(typeof config.taskTimeout === 'number' && config.taskTimeout > 0){
156
+ let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
157
+ let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
158
+ if(hasTimeout || hasRetry){
132
159
  workerTaskTimers[worker.id] = {
133
- timer: setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout),
160
+ timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
134
161
  subTask,
135
162
  startTimestamp
136
163
  };
@@ -152,10 +179,14 @@ const start = (config)=>{
152
179
  initWorker(worker);
153
180
  }
154
181
  });
155
- cluster.on('exit', (worker, code, signal) => {
182
+ cluster.on('exit', (worker, code, signal) => {
183
+ let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
156
184
  clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
157
185
  if (code !== 0 && !worker.exitedAfterDisconnect) {
158
186
  console.log('Worker died unexpectedly starting a new one...');
187
+ if(rec && rec.subTask){
188
+ handleCrashedTask(rec.subTask);
189
+ };
159
190
  let worker = cluster.fork();
160
191
  initWorker(worker);
161
192
  return;
@@ -252,7 +283,27 @@ function runTask(config, subTask, subTaskLog){
252
283
  };
253
284
  let hasError = errors.length > 0;
254
285
  if(hasError)console.warn('[Task Error]', subTask.subid, errors);
286
+ let retried = false;
287
+ if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
288
+ let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
289
+ if(retryCount <= config.maxTaskRetries){
290
+ console.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
291
+ TaskMgr.mvTask(subTask.subid, 'running', 'new', ()=>{});
292
+ retried = true;
293
+ };
294
+ };
255
295
  TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
296
+ if(retried){
297
+ resolve({
298
+ startTimestamp,
299
+ endTimestamp,
300
+ cost
301
+ });
302
+ return;
303
+ };
304
+ if(!hasError){
305
+ TaskMgr.deleteSavedResult(subTask.subid);//清掉前序尝试可能残留的(超时竞争)错误结果
306
+ };
256
307
  TaskMgr.saveTaskResult(subTask.subid, hasError, hasError ? errors : resultData);
257
308
  TaskMgr.mvTask(subTask.subid, 'running',(hasError ? 'finished_with_errors':'finished'), (newPath)=>{
258
309
  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;
@@ -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(()=>{