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 +206 -164
- package/package.json +1 -1
- package/utils/resolveWorkers.js +31 -0
- package/workers/Multithread.js +57 -4
- package/workers/TaskMgr.js +25 -1
- package/workers/asMaster.js +5 -3
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
|
-
###
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if(taskCount
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
###
|
|
163
|
-
|
|
164
|
-
|
|
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
|
@@ -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 };
|
package/workers/Multithread.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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({
|
package/workers/TaskMgr.js
CHANGED
|
@@ -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;
|
package/workers/asMaster.js
CHANGED
|
@@ -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
|
-
|
|
63
|
+
config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
|
|
62
64
|
|
|
63
65
|
let {taskRootFolder, taskName} = config;
|
|
64
66
|
|