multi-tasks 3.1.3 → 3.1.5
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 +314 -260
- package/package.json +2 -2
- package/utils/serializeFns.js +46 -0
- package/workers/TaskMgr.js +13 -3
- package/workers/asMaster.js +11 -6
- package/workers/index.js +65 -8
package/README.md
CHANGED
|
@@ -1,260 +1,314 @@
|
|
|
1
|
-
# multi-tasks
|
|
2
|
-
|
|
3
|
-
Multi-tasks is a toolkit to manage long-term and large-scale parallel computing tasks. Progress and tasks are stored on the file system, so tasks can be resumed even if the host crashes.
|
|
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
|
-
The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
|
|
16
|
-
|
|
17
|
-
**Entry functions:**
|
|
18
|
-
|
|
19
|
-
- **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
|
|
20
|
-
- **`multiTasks.
|
|
21
|
-
- **`multiTasks.
|
|
22
|
-
- **`multiTasks.restart(config)`** —
|
|
23
|
-
|
|
24
|
-
**Config options:**
|
|
25
|
-
|
|
26
|
-
- **`initialTasks`** — array of task objects, or a folder path string to resume from.
|
|
27
|
-
- **`processTask(task, helper)`** — required; return a value or a Promise.
|
|
28
|
-
- **`taskRootFolder`** —
|
|
29
|
-
- **`taskId`** — task folder name under `taskRootFolder`.
|
|
30
|
-
- **`
|
|
31
|
-
- **`
|
|
32
|
-
- **`
|
|
33
|
-
- **`
|
|
34
|
-
- **`
|
|
35
|
-
- **`
|
|
36
|
-
- **`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **`helper.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
let
|
|
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
|
-
return {
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1
|
+
# multi-tasks
|
|
2
|
+
|
|
3
|
+
Multi-tasks is a toolkit to manage long-term and large-scale parallel computing tasks. Progress and tasks are stored on the file system, so tasks can be resumed even if the host crashes.
|
|
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
|
+
The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
|
|
16
|
+
|
|
17
|
+
**Entry functions:**
|
|
18
|
+
|
|
19
|
+
- **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
|
|
20
|
+
- **`multiTasks.resume(config)`** — resume an interrupted run; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
|
|
21
|
+
- **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
|
|
22
|
+
- **`multiTasks.restart(config)`** — wipe the task folder's progress and re-run from scratch; pass a config object with `taskFolder`. The config (including `processTask` and `initialTasks`) is restored from `task_config.json`; any field you pass overrides the snapshot, and overrides are written back into it. **All previous progress and results are deleted.**
|
|
23
|
+
|
|
24
|
+
**Config options:**
|
|
25
|
+
|
|
26
|
+
- **`initialTasks`** — array of task objects, or a folder path string to resume from.
|
|
27
|
+
- **`processTask(task, helper)`** — required; return a value or a Promise.
|
|
28
|
+
- **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
|
|
29
|
+
- **`taskId`** — task folder name under `taskRootFolder`.
|
|
30
|
+
- **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
|
|
31
|
+
- **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
|
|
32
|
+
- **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
|
|
33
|
+
- **`autoCloseAfterCompletion`** — set `true` to kill workers and exit the process when no tasks are left; default is `false`, i.e. workers stay alive waiting for dynamically created tasks.
|
|
34
|
+
- **`shouldTerminate(info)`** — return `true` to terminate the whole process.
|
|
35
|
+
- **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
|
|
36
|
+
- **`onFinish(report)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`); `report` is an empty object for now.
|
|
37
|
+
|
|
38
|
+
**Other options:**
|
|
39
|
+
|
|
40
|
+
- **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`.
|
|
41
|
+
|
|
42
|
+
**Task helper** (the `helper` object passed as the second argument of `processTask`):
|
|
43
|
+
|
|
44
|
+
- **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
|
|
45
|
+
- **`helper.emit(event, payload, option?)`** — broadcast an event to all workers (relayed by the master, best-effort); `option.includingMe` defaults to `true` — pass `{includingMe: false}` to exclude the sender. Payload must be JSON-serializable.
|
|
46
|
+
- **`helper.setListener(event, handler)`** — listen for broadcast events; one handler per event per worker process (re-registering replaces it), so calling it inside `processTask` is always safe; `handler(payload, meta)` gets `meta.fromWorkerId` and `meta.fromWorkerPid`. Broadcasts are runtime-only messages — not persisted, not replayed on resume.
|
|
47
|
+
- **`helper.emitSys(event, payload?)`** — send a system event to the master (consumed by the master itself, not relayed). Built-in event: `'TERMINATE_ALL_WORKERS'` force-kills all workers and exits the master; any other event goes to `config.setSysListener`.
|
|
48
|
+
|
|
49
|
+
### How to use:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
//see examples/example0
|
|
53
|
+
let multiTasks = require('multi-tasks').multiTasks;
|
|
54
|
+
|
|
55
|
+
//Step1, create the tasks to run in parallel as an array.
|
|
56
|
+
let alltasks = [];
|
|
57
|
+
for(let i=0;i<50;i++){
|
|
58
|
+
alltasks.push({
|
|
59
|
+
name: `task-${i}`,
|
|
60
|
+
data: `This prop is for a subtask`
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//Step2, provide a function that processes each sub-task and returns the result
|
|
65
|
+
let processTask = (task, helper)=>{
|
|
66
|
+
let {taskCount} = task;//get your task data
|
|
67
|
+
|
|
68
|
+
//Run your processing logic here...
|
|
69
|
+
|
|
70
|
+
//then return the result as a plain javascript object or an async promise
|
|
71
|
+
return 'result data';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//Step3, run!
|
|
75
|
+
multiTasks({
|
|
76
|
+
initialTasks: alltasks,
|
|
77
|
+
processTask,
|
|
78
|
+
taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and result files; you can check the progress here
|
|
79
|
+
taskId: 'my-task',
|
|
80
|
+
numberOfWorkers: 3, //how many workers are working in parallel
|
|
81
|
+
//taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
|
|
82
|
+
//maxTaskRetries: 2, //optional, auto-retry a failed task (worker crash, processTask error, or timeout); retried tasks go back to the queue
|
|
83
|
+
//autoCloseAfterCompletion: true, //set true to exit the process when all tasks are done; keep it false (default) if you create new tasks dynamically
|
|
84
|
+
shouldTerminate:(info)=>{
|
|
85
|
+
//return true if you need to terminate the whole process
|
|
86
|
+
},
|
|
87
|
+
onFinish: (report)=>{
|
|
88
|
+
console.log('finish callback', report);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### More about the task processing function
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
//Examples 1-3 below are independent variants of processTask, use one at a time.
|
|
98
|
+
|
|
99
|
+
//Example1, return a promise for async processes,
|
|
100
|
+
// you can return a promise or a non-promise result,
|
|
101
|
+
// all result data can be found in the results/succ folder
|
|
102
|
+
let processTask = (task, helper)=>{
|
|
103
|
+
let {taskCount} = task;
|
|
104
|
+
|
|
105
|
+
return new Promise((resolve, reject)=>{
|
|
106
|
+
resolve({
|
|
107
|
+
data:`task${taskCount} complete`
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
//Example2, dynamically create a new task while processing
|
|
113
|
+
let processTask = (task, helper)=>{
|
|
114
|
+
let {taskCount} = task;
|
|
115
|
+
|
|
116
|
+
if(taskCount % 2 === 0){
|
|
117
|
+
//create a new task if needed
|
|
118
|
+
helper.createNewTasks({
|
|
119
|
+
msg:'a new task'
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return new Promise((resolve, reject)=>{
|
|
125
|
+
resolve({
|
|
126
|
+
data:`task${taskCount} complete`
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
//Example3, generate/throw exceptions in a task method
|
|
132
|
+
let processTask = (task, helper)=>{
|
|
133
|
+
let {taskCount} = task;
|
|
134
|
+
|
|
135
|
+
//Demo of exceptions/errors: they are captured and saved in the results/errors folder
|
|
136
|
+
if(taskCount===3) throw 'exception';
|
|
137
|
+
if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
|
|
138
|
+
if(taskCount===5) aaa = bbb;//this ReferenceError will be captured by multi-tasks
|
|
139
|
+
|
|
140
|
+
return {data:'succ'};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//Example4, timeout: a task that does not finish within taskTimeout is
|
|
144
|
+
// treated as failed with a timeout error
|
|
145
|
+
multiTasks({
|
|
146
|
+
initialTasks: alltasks,
|
|
147
|
+
processTask,
|
|
148
|
+
taskRootFolder: `../examples-tmp-data/example-timeout`,
|
|
149
|
+
taskId: 'my-task',
|
|
150
|
+
numberOfWorkers: 3,
|
|
151
|
+
taskTimeout: 30000,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
//Example5, auto-retry a failed task with maxTaskRetries
|
|
155
|
+
multiTasks({
|
|
156
|
+
initialTasks: alltasks,
|
|
157
|
+
processTask,
|
|
158
|
+
taskRootFolder: `../examples-tmp-data/example-retry`,
|
|
159
|
+
taskId: 'my-task',
|
|
160
|
+
numberOfWorkers: 3,
|
|
161
|
+
maxTaskRetries: 2,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Event broadcast
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
|
|
170
|
+
//Example6, broadcast events between workers with helper.emit/helper.setListener
|
|
171
|
+
multiTasks({
|
|
172
|
+
initialTasks: alltasks,
|
|
173
|
+
taskRootFolder: `../examples-tmp-data/example-broadcast`,
|
|
174
|
+
taskId: 'my-task',
|
|
175
|
+
numberOfWorkers: 3,
|
|
176
|
+
processTask: (task, helper) => {
|
|
177
|
+
helper.setListener('task-done', (payload, meta) => {
|
|
178
|
+
console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
|
|
179
|
+
});
|
|
180
|
+
helper.emit('task-done', {seq: task.seq});
|
|
181
|
+
return {data:'succ'};
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
//Example7, system events from a worker to the master with helper.emitSys
|
|
186
|
+
multiTasks({
|
|
187
|
+
initialTasks: alltasks,
|
|
188
|
+
taskRootFolder: `../examples-tmp-data/example-sysevent`,
|
|
189
|
+
taskId: 'my-task',
|
|
190
|
+
numberOfWorkers: 3,
|
|
191
|
+
setSysListener: (event, payload, meta) => {
|
|
192
|
+
console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
|
|
193
|
+
},
|
|
194
|
+
processTask: (task, helper) => {
|
|
195
|
+
helper.emitSys('task-started', {seq: task.seq});
|
|
196
|
+
if(task.fatal){
|
|
197
|
+
helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
|
|
198
|
+
return;
|
|
199
|
+
};
|
|
200
|
+
return {data:'succ'};
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Resuming
|
|
207
|
+
|
|
208
|
+
If the execution is interrupted (e.g. a power outage), the simplest way to resume is to just re-run the same code — `multiTasks(config)` detects the existing task folder and auto-resumes, re-running only the stuck tasks:
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
|
|
212
|
+
//same command, same config as the interrupted run — that's all
|
|
213
|
+
multiTasks(config);
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Or point `initialTasks` at the task folder — same effect:
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
|
|
221
|
+
multiTasks({ ...config, initialTasks: `/myworks/my_scan_tasks/my-task` });
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or resume from the task folder alone — the config (including `processTask`) is restored from `task_config.json`:
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
|
|
229
|
+
multiTasks.resume({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Note:** revived functions must be **self-contained** (no outer-scope variables; `require(...)` inside the body) — otherwise pass them again, they always win:
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
multiTasks.resume({
|
|
237
|
+
taskFolder: `/myworks/my_scan_tasks/my-task`,
|
|
238
|
+
processTask,
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Retrying failed tasks
|
|
243
|
+
|
|
244
|
+
To resume an interrupted run and also retry the tasks that failed (in task folder `progress/finished_with_errors`):
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
|
|
248
|
+
multiTasks.retry_fails({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Restarting from scratch
|
|
253
|
+
|
|
254
|
+
To throw away all progress and results in a task folder and re-run all the initial tasks from the beginning (the config, including `processTask` and `initialTasks`, is restored from `task_config.json`):
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
|
|
258
|
+
multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes all previous progress and results, then re-runs all the initial tasks
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **all the initial tasks** — tasks created dynamically via `helper.createNewTasks` are not restored and will not be re-run. Fields you pass (e.g. `processTask`) override the snapshot and are written back into it; the same self-contained-function note as in Resuming applies. (**Breaking change:** the name `restart` was the old name of `retry_fails` in v2 and in the published v3.0–v3.1.x releases; this is a different API — calling it no longer retries failed tasks, it wipes the folder and re-runs everything.)
|
|
263
|
+
|
|
264
|
+
### Changelog:
|
|
265
|
+
|
|
266
|
+
- 3.1.5 Update README
|
|
267
|
+
- 3.1.4 Rewrite `multiTasks.resume`, `multiTasks.retry_fails` and `multiTasks.restart`
|
|
268
|
+
- 3.1.3 Update README
|
|
269
|
+
- 3.1.2 Update README
|
|
270
|
+
- 3.1.1 Fix readme documentation
|
|
271
|
+
- 3.1.0 Support worker broadcast ('helper.emit' and 'helper.setListener') and system events ('helper.emitSys' and 'setSysListener', with built-in 'TERMINATE_ALL_WORKERS'); default numberOfWorkers is now "50%" of CPU cores (was core count minus 1)
|
|
272
|
+
- 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
|
|
273
|
+
- 3.0.3 Support 'maxTaskRetries'
|
|
274
|
+
- 3.0.2 Support 'taskTimeout'
|
|
275
|
+
- 3.0.1 Versions 3.0.0 and above are maintained by AI
|
|
276
|
+
- 2.2.0 Support Resuming from an interrupted task
|
|
277
|
+
- 2.1.2 Update readme
|
|
278
|
+
- 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
|
|
279
|
+
- 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
|
|
280
|
+
- 2.0.9 Update readme examples
|
|
281
|
+
- 2.0.8 Update readme examples
|
|
282
|
+
- 2.0.7 Update changelog
|
|
283
|
+
- 2.0.6 Update readme, remove failed examples
|
|
284
|
+
- 2.0.5 Update readme examples
|
|
285
|
+
- 2.0.4 Support resume from a failed task
|
|
286
|
+
- 2.0.3 Fix: mkdir bug on windows
|
|
287
|
+
- 2.0.2 new feature: support shouldTerminate
|
|
288
|
+
- 2.0.1 Avoid possible I/O conflicts.
|
|
289
|
+
- 2.0.0 Rewritten with a new architecture to support dynamic tasks.
|
|
290
|
+
- 1.2.8 Fix: create task folder failed on MacOS
|
|
291
|
+
- 1.2.7 Small updates
|
|
292
|
+
- 1.2.6 Support onFinish event
|
|
293
|
+
- 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
|
|
294
|
+
- 1.2.4 Fix: opt.numberOfWorkers not work
|
|
295
|
+
- 1.2.3 Update README
|
|
296
|
+
- 1.2.2 Update README and examples
|
|
297
|
+
- 1.2.1 Handle exceptions and errors in subtasks
|
|
298
|
+
- 1.2.0 Simplified usage by providing the function way and support return Promise
|
|
299
|
+
- 1.1.4 Remove make-dir
|
|
300
|
+
- 1.1.3 Simplified usage, see example0
|
|
301
|
+
- 1.1.2
|
|
302
|
+
- 1.1.1 Rename files, updated changelog
|
|
303
|
+
- 1.1.0 Simplified the usage of a customized Consumer, see example0
|
|
304
|
+
- 1.0.8 Fix examples
|
|
305
|
+
- 1.0.7 Remove moment
|
|
306
|
+
- 1.0.6 Performance optimization
|
|
307
|
+
|
|
308
|
+
### Gitee:
|
|
309
|
+
|
|
310
|
+
[https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
|
|
311
|
+
|
|
312
|
+
### License:
|
|
313
|
+
|
|
314
|
+
[MIT](https://opensource.org/license/MIT)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multi-tasks",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.5",
|
|
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": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"example": "examples"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=14"
|
|
19
|
+
"node": ">=14.14"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"test": "jest",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// config 里的函数型配置键: 写 task_config.json 快照时序列化为源码字符串,
|
|
4
|
+
// resume 时复活为函数。未来新增函数型配置键在此登记。
|
|
5
|
+
const FUNCTION_CONFIG_KEYS = ['processTask', 'onFinish', 'shouldTerminate', 'setSysListener'];
|
|
6
|
+
|
|
7
|
+
//返回浅副本: 函数型键的函数值替换为源码字符串, 其余值原样保留。
|
|
8
|
+
const serializeConfig = (config) => {
|
|
9
|
+
const out = { ...config };
|
|
10
|
+
FUNCTION_CONFIG_KEYS.forEach((key) => {
|
|
11
|
+
if (typeof out[key] === 'function') {
|
|
12
|
+
out[key] = out[key].toString();
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//返回浅副本: 函数型键的字符串值复活为函数。复活环境注入 require/module/exports,
|
|
19
|
+
//函数体内可直接 require(...)。eval 失败或复活结果非函数时抛 Error(消息带键名)。
|
|
20
|
+
const reviveConfig = (config) => {
|
|
21
|
+
const out = { ...config };
|
|
22
|
+
FUNCTION_CONFIG_KEYS.forEach((key) => {
|
|
23
|
+
if (typeof out[key] === 'string') {
|
|
24
|
+
const src = out[key];
|
|
25
|
+
let revived;
|
|
26
|
+
try {
|
|
27
|
+
revived = new Function('require', 'module', 'exports', `return (${src}\n);`)(require, module, exports);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
//方法简写(如 "processTask(task) {...}" 或 "async processTask(task) {...}")需包成函数声明再复活
|
|
30
|
+
let decl = src.startsWith('async ') ? `async function ${src.slice('async '.length)}` : `function ${src}`;
|
|
31
|
+
try {
|
|
32
|
+
revived = new Function('require', 'module', 'exports', `return (${decl}\n);`)(require, module, exports);
|
|
33
|
+
} catch (e2) {
|
|
34
|
+
throw new Error(`Failed to revive config function "${key}" from task_config.json: ${e.message}`);
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
if (typeof revived !== 'function') {
|
|
38
|
+
throw new Error(`Failed to revive config function "${key}" from task_config.json: source is not a function`);
|
|
39
|
+
};
|
|
40
|
+
out[key] = revived;
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
return out;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
module.exports = { FUNCTION_CONFIG_KEYS, serializeConfig, reviveConfig };
|
package/workers/TaskMgr.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const pathutil = require('path');
|
|
3
3
|
const makeDir = require('../utils/makedir');
|
|
4
4
|
const randomsUtil = require('../utils/randoms');
|
|
5
|
+
const { serializeConfig } = require('../utils/serializeFns');
|
|
5
6
|
|
|
6
7
|
let allSubTasks = {
|
|
7
8
|
new:[],
|
|
@@ -20,13 +21,16 @@ const TaskMgr = {
|
|
|
20
21
|
initFolders:()=>{
|
|
21
22
|
let {taskRootFolder, masterTaskId} = main_config;
|
|
22
23
|
const taskFolder = pathutil.resolve(taskRootFolder, masterTaskId);
|
|
23
|
-
if(fs.existsSync(taskFolder) && !(main_config.__resume || main_config.__restart)){
|
|
24
|
+
if(fs.existsSync(taskFolder) && !(main_config.__resume || main_config.__retryFails || main_config.__restart)){
|
|
24
25
|
let files = fs.readdirSync(taskFolder);
|
|
25
26
|
if(files.length > 0){
|
|
26
27
|
console.log('[FATAL] taskRootFolder already exist and not empty, delete it and try again: ', taskFolder);
|
|
27
28
|
return process.exit(0);
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
if(main_config.__restart && fs.existsSync(taskFolder)){
|
|
32
|
+
TaskMgr.resetTaskFolders(taskFolder);//restart: 整目录重置, 随后由下方 makeDir 重建
|
|
33
|
+
};
|
|
30
34
|
const progressRootFolder = pathutil.resolve(taskFolder, './progress');
|
|
31
35
|
const resultsRootFolder = pathutil.resolve(taskFolder, './results');
|
|
32
36
|
const logsRootFolder = pathutil.resolve(taskFolder, './logs');
|
|
@@ -61,9 +65,12 @@ const TaskMgr = {
|
|
|
61
65
|
taskFolderResultFailed,
|
|
62
66
|
};
|
|
63
67
|
|
|
64
|
-
//for
|
|
65
|
-
let main_config2 = JSON.parse(JSON.stringify(main_config))
|
|
68
|
+
//for resuming an interrupted task
|
|
69
|
+
let main_config2 = JSON.parse(JSON.stringify(serializeConfig(main_config)));//函数型配置键先序列化为源码字符串, 供 resume 从快照复活
|
|
66
70
|
delete main_config2.tasks;//will load from folder 'new', so no need the early tasks data
|
|
71
|
+
Object.keys(main_config2).forEach((key)=>{
|
|
72
|
+
if(key.startsWith('__')) delete main_config2[key];//__-prefixed keys are one-shot runtime markers, never persisted
|
|
73
|
+
});
|
|
67
74
|
fs.writeFileSync(pathutil.resolve(taskFolder, 'task_config.json'), JSON.stringify(main_config2));
|
|
68
75
|
return main_config;
|
|
69
76
|
},
|
|
@@ -250,5 +257,8 @@ const TaskMgr = {
|
|
|
250
257
|
delete task.__retryCount;
|
|
251
258
|
fs.writeFileSync(fpath, JSON.stringify(task));
|
|
252
259
|
},
|
|
260
|
+
resetTaskFolders:(taskFolder)=>{
|
|
261
|
+
fs.rmSync(taskFolder, {recursive: true, force: true});
|
|
262
|
+
},
|
|
253
263
|
};
|
|
254
264
|
module.exports = TaskMgr;
|
package/workers/asMaster.js
CHANGED
|
@@ -10,12 +10,17 @@ const resume = (config)=>{
|
|
|
10
10
|
let {taskRootFolder, taskId} = config;
|
|
11
11
|
let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
|
|
12
12
|
|
|
13
|
-
//load from config file
|
|
13
|
+
//load from config file: 旧快照作底(兼作任务目录合法性校验, 不存在抛 ENOENT),
|
|
14
|
+
//本次调用传入的配置覆盖同名字段——快照只补未传入的字段
|
|
14
15
|
let task_config = JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json')));
|
|
16
|
+
Object.keys(task_config).forEach((key)=>{
|
|
17
|
+
if(key.startsWith('__')) delete task_config[key];//读侧剔除一次性标记, 兼容 <=3.1.3 旧快照
|
|
18
|
+
});
|
|
15
19
|
config = {
|
|
16
|
-
...
|
|
17
|
-
...
|
|
18
|
-
}
|
|
20
|
+
...task_config,
|
|
21
|
+
...config
|
|
22
|
+
};
|
|
23
|
+
config.masterTaskId = taskId;
|
|
19
24
|
|
|
20
25
|
TaskMgr.load(config);
|
|
21
26
|
config = TaskMgr.initFolders(config);
|
|
@@ -29,11 +34,11 @@ const resume = (config)=>{
|
|
|
29
34
|
let subid = fname.replace(/\.task\.json$/, '');
|
|
30
35
|
promises.push(TaskMgr.mvTaskPromise(subid, 'running', 'new'));
|
|
31
36
|
});
|
|
32
|
-
if(config.
|
|
37
|
+
if(config.__retryFails){
|
|
33
38
|
errfiles.forEach((fname)=>{
|
|
34
39
|
let subid = fname.replace(/\.task\.json$/, '');
|
|
35
40
|
promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
|
|
36
|
-
TaskMgr.resetTaskRetry(subid);//
|
|
41
|
+
TaskMgr.resetTaskRetry(subid);//retry_fails 重跑失败任务时重置重试计数(resume 不重置)
|
|
37
42
|
}));
|
|
38
43
|
});
|
|
39
44
|
};
|
package/workers/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const pathUtil = require('path');
|
|
3
3
|
const Multithread = require('./Multithread');
|
|
4
4
|
const backwardv1 = require('../utils/backward-v1');
|
|
5
|
+
const { FUNCTION_CONFIG_KEYS, reviveConfig } = require('../utils/serializeFns');
|
|
5
6
|
|
|
6
7
|
const _toAbsPath = (path)=>{
|
|
7
8
|
if(!path) return path;
|
|
@@ -10,6 +11,38 @@ const _toAbsPath = (path)=>{
|
|
|
10
11
|
return abspath;
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
const _normalizeResumeArg = (method, config)=>{
|
|
15
|
+
if(typeof config === 'string'){
|
|
16
|
+
throw new Error(`method ${method} need a config object with param "taskFolder" (string path form is no longer supported)`);
|
|
17
|
+
};
|
|
18
|
+
return backwardv1.v1tov2(config);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
//读取任务目录里的 task_config.json 快照, 复活其中的函数字符串后作底;
|
|
22
|
+
//用户传入的同名字段优先(合并结果会随 resume 被 initFolders 重写回快照, 成为新基线)。
|
|
23
|
+
const _mergeSnapshotConfig = (config)=>{
|
|
24
|
+
let snapshot = JSON.parse(fs.readFileSync(pathUtil.resolve(config.taskFolder, 'task_config.json')));
|
|
25
|
+
Object.keys(snapshot).forEach((key)=>{
|
|
26
|
+
if(key.startsWith('__')) delete snapshot[key];//读侧同样剔除一次性标记: 兼容 <=3.1.3 旧版本写出的带标记快照(防 stale 标记误触发 wipe/错走链路)
|
|
27
|
+
});
|
|
28
|
+
let userConfig = { ...config };
|
|
29
|
+
if(typeof userConfig.initialTasks === 'string'){
|
|
30
|
+
delete userConfig.initialTasks;//目录指针用法(auto-resume), 不应覆盖/污染快照里的任务数组
|
|
31
|
+
};
|
|
32
|
+
//用户已传入同名函数时, 快照里的源码字符串无需复活(反正会被覆盖)——容忍不可复活的源码(如绑定函数)
|
|
33
|
+
FUNCTION_CONFIG_KEYS.forEach((key)=>{
|
|
34
|
+
if(typeof userConfig[key] === 'function' && typeof snapshot[key] === 'string'){
|
|
35
|
+
delete snapshot[key];
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
let revived = reviveConfig(snapshot);
|
|
39
|
+
let merged = { ...revived, ...userConfig };
|
|
40
|
+
if(typeof merged.processTask !== 'function'){
|
|
41
|
+
throw new Error('FATAL: no processTask available: task_config.json has none and none was passed in config.');
|
|
42
|
+
};
|
|
43
|
+
return merged;
|
|
44
|
+
};
|
|
45
|
+
|
|
13
46
|
const start = (config)=>{
|
|
14
47
|
config = backwardv1.v1tov2(config);
|
|
15
48
|
|
|
@@ -19,28 +52,51 @@ const start = (config)=>{
|
|
|
19
52
|
Multithread.start(config);
|
|
20
53
|
};
|
|
21
54
|
const resume = (config)=>{
|
|
22
|
-
config =
|
|
55
|
+
config = _normalizeResumeArg('resume', config);
|
|
23
56
|
config.__resume = true;
|
|
24
57
|
|
|
25
|
-
config.initialTasks = [];
|
|
26
|
-
|
|
27
58
|
config.taskRootFolder = _toAbsPath(config.taskRootFolder);
|
|
28
59
|
config.taskFolder = _toAbsPath(config.taskFolder);
|
|
60
|
+
if(!config.taskFolder){
|
|
61
|
+
if(!config.taskRootFolder || !config.taskId){
|
|
62
|
+
console.log('[ERROR]', 'method resume need param "taskFolder" (or "taskRootFolder" + "taskId")');
|
|
63
|
+
return;
|
|
64
|
+
};
|
|
65
|
+
config.taskFolder = pathUtil.resolve(config.taskRootFolder, config.taskId);
|
|
66
|
+
};
|
|
67
|
+
config.taskRootFolder = pathUtil.dirname(config.taskFolder);
|
|
68
|
+
config.taskId = pathUtil.basename(config.taskFolder);
|
|
29
69
|
|
|
70
|
+
config = _mergeSnapshotConfig(config);
|
|
71
|
+
Multithread.start(config);
|
|
72
|
+
};
|
|
73
|
+
const retry_fails = (config)=>{
|
|
74
|
+
config = _normalizeResumeArg('retry_fails', config);
|
|
75
|
+
let taskFolder = _toAbsPath(config.taskFolder);
|
|
76
|
+
if(!taskFolder){
|
|
77
|
+
console.log('[ERROR]', 'method retry_fails need param "taskFolder"');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
config.taskFolder = taskFolder;
|
|
81
|
+
config.taskRootFolder = pathUtil.dirname(taskFolder);
|
|
82
|
+
config.taskId = pathUtil.basename(taskFolder);
|
|
83
|
+
config.__resume = true;
|
|
84
|
+
config.__retryFails = true;
|
|
85
|
+
config = _mergeSnapshotConfig(config);
|
|
30
86
|
Multithread.start(config);
|
|
31
87
|
};
|
|
32
88
|
const restart = (config)=>{
|
|
33
|
-
config =
|
|
34
|
-
let
|
|
89
|
+
config = _normalizeResumeArg('restart', config);
|
|
90
|
+
let taskFolder = _toAbsPath(config.taskFolder);
|
|
35
91
|
if(!taskFolder){
|
|
36
92
|
console.log('[ERROR]', 'method restart need param "taskFolder"');
|
|
37
93
|
return;
|
|
38
94
|
}
|
|
95
|
+
config.taskFolder = taskFolder;
|
|
39
96
|
config.taskRootFolder = pathUtil.dirname(taskFolder);
|
|
40
97
|
config.taskId = pathUtil.basename(taskFolder);
|
|
41
|
-
config.__resume = true;
|
|
42
98
|
config.__restart = true;
|
|
43
|
-
config
|
|
99
|
+
config = _mergeSnapshotConfig(config);
|
|
44
100
|
Multithread.start(config);
|
|
45
101
|
};
|
|
46
102
|
|
|
@@ -77,7 +133,8 @@ const multiTasks = (config)=>{
|
|
|
77
133
|
};
|
|
78
134
|
|
|
79
135
|
multiTasks.start = start;
|
|
80
|
-
multiTasks.
|
|
136
|
+
multiTasks.retry_fails = retry_fails;
|
|
81
137
|
multiTasks.resume = resume;
|
|
138
|
+
multiTasks.restart = restart;
|
|
82
139
|
|
|
83
140
|
module.exports = multiTasks;
|