multi-tasks 3.0.1 → 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 +201 -160
- package/package.json +1 -1
- package/workers/Multithread.js +99 -2
- package/workers/TaskMgr.js +33 -1
- package/workers/asMaster.js +3 -1
package/README.md
CHANGED
|
@@ -1,160 +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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
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
|
-
multiTasks(
|
|
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
|
-
|
|
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
|
-
|
|
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
package/workers/Multithread.js
CHANGED
|
@@ -27,6 +27,8 @@ let USER_CONFIG;
|
|
|
27
27
|
|
|
28
28
|
let taskCount = 0;
|
|
29
29
|
|
|
30
|
+
let workerTaskTimers = {};//worker.id -> {timer, subTask, startTimestamp}, master 端任务超时倒计时
|
|
31
|
+
|
|
30
32
|
let killAllWorkers = ()=>{
|
|
31
33
|
for (const id in cluster.workers) {
|
|
32
34
|
if (cluster.workers.hasOwnProperty(id)) {
|
|
@@ -56,10 +58,70 @@ const start = (config)=>{
|
|
|
56
58
|
const AsyncQueue = require(`../utils/asyncQueue`);
|
|
57
59
|
const queue = new AsyncQueue();
|
|
58
60
|
|
|
61
|
+
let clearTaskTimer = (workerId)=>{
|
|
62
|
+
let rec = workerTaskTimers[workerId];
|
|
63
|
+
if(rec){
|
|
64
|
+
clearTimeout(rec.timer);
|
|
65
|
+
delete workerTaskTimers[workerId];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
let finalizeTimeoutTask = (subTask, cost)=>{
|
|
69
|
+
let subid = subTask.subid;
|
|
70
|
+
try{
|
|
71
|
+
let resultType = TaskMgr.getSavedResultType(subid);
|
|
72
|
+
if(resultType === 'succ'){//超时边界: worker 已成功写盘, 视为已完成
|
|
73
|
+
TaskMgr.mvTask(subid, 'running', 'finished', ()=>{});
|
|
74
|
+
}else if(resultType === 'error'){//超时边界: worker 已自行记错误
|
|
75
|
+
TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
|
|
76
|
+
}else{
|
|
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}]);
|
|
86
|
+
TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
|
|
87
|
+
}
|
|
88
|
+
}catch(e){
|
|
89
|
+
console.warn('[Master]: failed to finalize timeout task', subid, e);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let onTaskTimeout = (worker)=>{
|
|
93
|
+
let rec = workerTaskTimers[worker.id];
|
|
94
|
+
if(!rec) return;
|
|
95
|
+
delete workerTaskTimers[worker.id];
|
|
96
|
+
let {subTask, startTimestamp} = rec;
|
|
97
|
+
let cost = new Date()*1 - startTimestamp;
|
|
98
|
+
console.warn(`[Master]: task "${subTask.subid}" timeout after ${cost}ms, killing worker pid=${worker.process.pid}`);
|
|
99
|
+
worker.kill();//不 disconnect: exit 事件会按崩溃补员
|
|
100
|
+
finalizeTimeoutTask(subTask, cost);
|
|
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
|
+
};
|
|
119
|
+
|
|
59
120
|
let initWorker = (worker)=>{
|
|
60
121
|
WorkerMgr.addWorker(worker);
|
|
61
122
|
worker.on('message', function(message) {
|
|
62
123
|
if(message === MSG_REQUEST_NEW_TASK){
|
|
124
|
+
clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
|
|
63
125
|
queue.enqueue(async ()=>{
|
|
64
126
|
return TaskMgr.updateNewTasks();
|
|
65
127
|
});//use queue to make sure no I/O conflict
|
|
@@ -82,14 +144,24 @@ const start = (config)=>{
|
|
|
82
144
|
return process.exit(0);
|
|
83
145
|
}
|
|
84
146
|
//send new tasks to the worker
|
|
147
|
+
let startTimestamp = new Date()*1;
|
|
85
148
|
worker.send({
|
|
86
149
|
taskCount,
|
|
87
|
-
startTimestamp
|
|
150
|
+
startTimestamp,
|
|
88
151
|
config,
|
|
89
152
|
subTask
|
|
90
153
|
},()=>{}, {
|
|
91
154
|
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
92
155
|
});
|
|
156
|
+
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
157
|
+
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
158
|
+
if(hasTimeout || hasRetry){
|
|
159
|
+
workerTaskTimers[worker.id] = {
|
|
160
|
+
timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
|
|
161
|
+
subTask,
|
|
162
|
+
startTimestamp
|
|
163
|
+
};
|
|
164
|
+
}
|
|
93
165
|
taskCount++;
|
|
94
166
|
}
|
|
95
167
|
});
|
|
@@ -107,9 +179,14 @@ const start = (config)=>{
|
|
|
107
179
|
initWorker(worker);
|
|
108
180
|
}
|
|
109
181
|
});
|
|
110
|
-
cluster.on('exit', (worker, code, signal) => {
|
|
182
|
+
cluster.on('exit', (worker, code, signal) => {
|
|
183
|
+
let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
|
|
184
|
+
clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
|
|
111
185
|
if (code !== 0 && !worker.exitedAfterDisconnect) {
|
|
112
186
|
console.log('Worker died unexpectedly starting a new one...');
|
|
187
|
+
if(rec && rec.subTask){
|
|
188
|
+
handleCrashedTask(rec.subTask);
|
|
189
|
+
};
|
|
113
190
|
let worker = cluster.fork();
|
|
114
191
|
initWorker(worker);
|
|
115
192
|
return;
|
|
@@ -206,7 +283,27 @@ function runTask(config, subTask, subTaskLog){
|
|
|
206
283
|
};
|
|
207
284
|
let hasError = errors.length > 0;
|
|
208
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
|
+
};
|
|
209
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
|
+
};
|
|
210
307
|
TaskMgr.saveTaskResult(subTask.subid, hasError, hasError ? errors : resultData);
|
|
211
308
|
TaskMgr.mvTask(subTask.subid, 'running',(hasError ? 'finished_with_errors':'finished'), (newPath)=>{
|
|
212
309
|
resolve({
|
package/workers/TaskMgr.js
CHANGED
|
@@ -205,6 +205,14 @@ const TaskMgr = {
|
|
|
205
205
|
};
|
|
206
206
|
fs.writeFileSync(fpath, fcontent);
|
|
207
207
|
},
|
|
208
|
+
getSavedResultType:(subid)=>{
|
|
209
|
+
let {taskFolderResult, taskFolderResultFailed} = main_config;
|
|
210
|
+
let succPath = pathutil.resolve(taskFolderResult, `${subid}.json`);
|
|
211
|
+
let errPath = pathutil.resolve(taskFolderResultFailed, `${subid}.json`);
|
|
212
|
+
if(fs.existsSync(succPath)) return 'succ';
|
|
213
|
+
if(fs.existsSync(errPath)) return 'error';
|
|
214
|
+
return null;
|
|
215
|
+
},
|
|
208
216
|
saveTaskLog:(subid, log)=>{
|
|
209
217
|
let {logsRootFolder} = main_config;
|
|
210
218
|
let folder = logsRootFolder;
|
|
@@ -217,6 +225,30 @@ const TaskMgr = {
|
|
|
217
225
|
fcontent = JSON.stringify(log);
|
|
218
226
|
};
|
|
219
227
|
fs.writeFileSync(fpath, fcontent);
|
|
220
|
-
}
|
|
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
|
+
},
|
|
221
253
|
};
|
|
222
254
|
module.exports = TaskMgr;
|
package/workers/asMaster.js
CHANGED
|
@@ -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(()=>{
|