multi-tasks 3.0.4 → 3.1.1
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 +78 -18
- package/package.json +1 -1
- package/utils/resolveWorkers.js +7 -3
- package/workers/Multithread.js +35 -0
- package/workers/helper.js +61 -22
package/README.md
CHANGED
|
@@ -12,23 +12,36 @@ npm install multi-tasks
|
|
|
12
12
|
|
|
13
13
|
### API:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.start(config)`** — always start a fresh run.
|
|
21
|
+
- **`multiTasks.resume(config)`** — resume an interrupted run.
|
|
22
|
+
- **`multiTasks.restart(config)`** — resume and also retry failed tasks (needs `config.taskFolder`).
|
|
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`** — root directory where progress and result files are stored.
|
|
29
|
+
- **`taskId`** — task folder name under `taskRootFolder`.
|
|
30
|
+
- **`taskFolder`** — full task folder path, required by `restart`.
|
|
31
|
+
- **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
|
|
32
|
+
- **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
|
|
33
|
+
- **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
|
|
34
|
+
- **`autoCloseAfterCompletion`** — set `false` if you create new tasks dynamically.
|
|
35
|
+
- **`shouldTerminate(info)`** — return `true` to terminate the whole process.
|
|
36
|
+
- **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
|
|
37
|
+
- **`onFinish(report)`** — called once after all workers are done.
|
|
38
|
+
|
|
39
|
+
**Task helper** (the `helper` object passed as the second argument of `processTask`):
|
|
40
|
+
|
|
41
|
+
- **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
|
|
42
|
+
- **`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.
|
|
43
|
+
- **`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.
|
|
44
|
+
- **`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`.
|
|
32
45
|
|
|
33
46
|
### How to use:
|
|
34
47
|
|
|
@@ -138,6 +151,51 @@ multiTasks({
|
|
|
138
151
|
maxTaskRetries: 2,
|
|
139
152
|
});
|
|
140
153
|
|
|
154
|
+
//Example6, broadcast events between workers:
|
|
155
|
+
// a worker emits an event and the master relays it to every worker
|
|
156
|
+
// (best-effort, runtime only - not persisted, not replayed on
|
|
157
|
+
// resume); by default the sender also receives its own event, pass
|
|
158
|
+
// {includingMe: false} to exclude it; each event keeps only one
|
|
159
|
+
// listener per worker process, so calling setListener on every
|
|
160
|
+
// task is safe
|
|
161
|
+
multiTasks({
|
|
162
|
+
initialTasks: alltasks,
|
|
163
|
+
taskRootFolder: `../examples-tmp-data/example-broadcast`,
|
|
164
|
+
taskId: 'my-task',
|
|
165
|
+
numberOfWorkers: 3,
|
|
166
|
+
processTask: (task, helper) => {
|
|
167
|
+
helper.setListener('task-done', (payload, meta) => {
|
|
168
|
+
console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
|
|
169
|
+
});
|
|
170
|
+
helper.emit('task-done', {seq: task.seq});
|
|
171
|
+
return {data:'succ'};
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
//Example7, system events from a worker to the master:
|
|
176
|
+
// helper.emitSys sends a system event that the master consumes
|
|
177
|
+
// itself (not relayed to workers); the built-in event
|
|
178
|
+
// TERMINATE_ALL_WORKERS force-kills all workers and exits the
|
|
179
|
+
// master (unfinished tasks stay for resume); any other event is
|
|
180
|
+
// passed to config.setSysListener on the master
|
|
181
|
+
multiTasks({
|
|
182
|
+
initialTasks: alltasks,
|
|
183
|
+
taskRootFolder: `../examples-tmp-data/example-sysevent`,
|
|
184
|
+
taskId: 'my-task',
|
|
185
|
+
numberOfWorkers: 3,
|
|
186
|
+
setSysListener: (event, payload, meta) => {
|
|
187
|
+
console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
|
|
188
|
+
},
|
|
189
|
+
processTask: (task, helper) => {
|
|
190
|
+
helper.emitSys('task-started', {seq: task.seq});
|
|
191
|
+
if(task.fatal){
|
|
192
|
+
helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
|
|
193
|
+
return;
|
|
194
|
+
};
|
|
195
|
+
return {data:'succ'};
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
141
199
|
```
|
|
142
200
|
|
|
143
201
|
### Resuming
|
|
@@ -161,6 +219,8 @@ The usage patterns documented above are covered by automated tests (unit tests i
|
|
|
161
219
|
|
|
162
220
|
### Changelog:
|
|
163
221
|
|
|
222
|
+
- 3.1.1 Fix readme documentation
|
|
223
|
+
- 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)
|
|
164
224
|
- 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
|
|
165
225
|
- 3.0.3 Support 'maxTaskRetries'
|
|
166
226
|
- 3.0.2 Support 'taskTimeout'
|
|
@@ -203,4 +263,4 @@ The usage patterns documented above are covered by automated tests (unit tests i
|
|
|
203
263
|
|
|
204
264
|
### License:
|
|
205
265
|
|
|
206
|
-
[MIT](https://opensource.org/
|
|
266
|
+
[MIT](https://opensource.org/license/MIT) (see [LICENSE](LICENSE))
|
package/package.json
CHANGED
package/utils/resolveWorkers.js
CHANGED
|
@@ -5,13 +5,17 @@ const numCPUs = require('os').cpus().length;
|
|
|
5
5
|
const PERCENT_PATTERN = /^(\d+(?:\.\d+)?)%$/;
|
|
6
6
|
|
|
7
7
|
// 解析 numberOfWorkers 配置:
|
|
8
|
-
// - undefined ->
|
|
8
|
+
// - undefined -> 默认 "50%", 按核数四舍五入折算, 最小为 1;
|
|
9
9
|
// - number -> 原样返回(现状行为不变);
|
|
10
10
|
// - string -> 必须是 "数字%" 形式且百分比在 1-100 之间, 按核数四舍五入折算, 最小为 1;
|
|
11
11
|
// - 其余 -> 抛 Error。
|
|
12
|
+
const percentToWorkers = (percent) => {
|
|
13
|
+
return Math.max(1, Math.round(numCPUs * percent / 100));
|
|
14
|
+
};
|
|
15
|
+
|
|
12
16
|
const resolveNumberOfWorkers = (value) => {
|
|
13
17
|
if (typeof value === 'undefined') {
|
|
14
|
-
return
|
|
18
|
+
return percentToWorkers(50);
|
|
15
19
|
};
|
|
16
20
|
if (typeof value === 'number') {
|
|
17
21
|
return value;
|
|
@@ -21,7 +25,7 @@ const resolveNumberOfWorkers = (value) => {
|
|
|
21
25
|
if (match) {
|
|
22
26
|
const percent = parseFloat(match[1]);
|
|
23
27
|
if (percent >= 1 && percent <= 100) {
|
|
24
|
-
return
|
|
28
|
+
return percentToWorkers(percent);
|
|
25
29
|
};
|
|
26
30
|
};
|
|
27
31
|
};
|
package/workers/Multithread.js
CHANGED
|
@@ -122,6 +122,37 @@ const start = (config)=>{
|
|
|
122
122
|
let initWorker = (worker)=>{
|
|
123
123
|
WorkerMgr.addWorker(worker);
|
|
124
124
|
worker.on('message', function(message) {
|
|
125
|
+
if(message && message.__sysEvent){//worker 系统事件: master 自己消费, 不转发
|
|
126
|
+
if(message.event === 'TERMINATE_ALL_WORKERS'){
|
|
127
|
+
console.warn(`[Master]: TERMINATE_ALL_WORKERS from worker ${worker.id}, killing all workers`);
|
|
128
|
+
killAllWorkers();
|
|
129
|
+
return process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
|
|
132
|
+
if(typeof USER_CONFIG.setSysListener === 'function'){
|
|
133
|
+
try{
|
|
134
|
+
USER_CONFIG.setSysListener(message.event, message.payload, meta);
|
|
135
|
+
}catch(e){
|
|
136
|
+
console.error(`[Master]: setSysListener for event "${message.event}" threw:`, e);
|
|
137
|
+
}
|
|
138
|
+
}else{
|
|
139
|
+
console.warn(`[Master]: unknown sys event "${message.event}" from worker ${worker.id} (no setSysListener configured)`);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if(message && message.__broadcast){//worker 广播请求: master 中转给所有 worker
|
|
144
|
+
let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
|
|
145
|
+
for (const id in cluster.workers) {
|
|
146
|
+
let w = cluster.workers[id];
|
|
147
|
+
if(w.id === worker.id && message.includingMe === false) continue;
|
|
148
|
+
try{
|
|
149
|
+
w.send({__broadcast: true, event: message.event, payload: message.payload, meta});
|
|
150
|
+
}catch(e){
|
|
151
|
+
console.warn(`[Master]: failed to relay broadcast to worker ${w.id}`, e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
125
156
|
if(message === MSG_REQUEST_NEW_TASK){
|
|
126
157
|
clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
|
|
127
158
|
queue.enqueue(async ()=>{
|
|
@@ -215,6 +246,10 @@ const start = (config)=>{
|
|
|
215
246
|
function subWorker(){
|
|
216
247
|
let worker = cluster.worker;
|
|
217
248
|
worker.on('message', function(message) {
|
|
249
|
+
if(message && message.__broadcast){//master 中转来的广播: 分发给 helper.on 注册的 handler
|
|
250
|
+
helper.__dispatchBroadcast(message);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
218
253
|
if(message === MSG_NO_TASK_FOUND){//wait for new task
|
|
219
254
|
console.log(` (Worker #${worker.id}): waiting`);
|
|
220
255
|
setTimeout(()=>{
|
package/workers/helper.js
CHANGED
|
@@ -1,22 +1,61 @@
|
|
|
1
|
-
let fs = require('fs');
|
|
2
|
-
let pathutil = require('path');
|
|
3
|
-
|
|
4
|
-
let TaskMgr = require('./TaskMgr');
|
|
5
|
-
|
|
6
|
-
let myconfig;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
let fs = require('fs');
|
|
2
|
+
let pathutil = require('path');
|
|
3
|
+
|
|
4
|
+
let TaskMgr = require('./TaskMgr');
|
|
5
|
+
|
|
6
|
+
let myconfig;
|
|
7
|
+
let listeners = {};//event -> handler, worker 进程内模块级广播监听 registry(每个 event 一个 handler)
|
|
8
|
+
|
|
9
|
+
const load = (config)=>{
|
|
10
|
+
myconfig = config;
|
|
11
|
+
};
|
|
12
|
+
const createNewTasks = (tasks)=>{
|
|
13
|
+
if (!myconfig) throw new Error('Please call helper.load(config) first!');
|
|
14
|
+
if (!Array.isArray(tasks)) tasks = [tasks];
|
|
15
|
+
TaskMgr.load(myconfig);
|
|
16
|
+
tasks.forEach((task)=>{
|
|
17
|
+
console.log('Creating new task', task)
|
|
18
|
+
TaskMgr.createNewTask(task);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//广播事件给所有 worker(经 master 中转); option.includingMe 默认 true(也发给自己)
|
|
23
|
+
const emit = (event, payload, option)=>{
|
|
24
|
+
if (typeof event !== 'string' || !event) throw new Error('helper.emit: event must be a non-empty string');
|
|
25
|
+
if (typeof process.send !== 'function') throw new Error('helper.emit: broadcast is only available in worker processes');
|
|
26
|
+
let includingMe = !option || option.includingMe !== false;
|
|
27
|
+
process.send({__broadcast: true, event, payload, includingMe});
|
|
28
|
+
};
|
|
29
|
+
//发送系统事件给 master(master 自己消费, 不转发); 内建事件 TERMINATE_ALL_WORKERS,
|
|
30
|
+
//其余交给 config.setSysListener
|
|
31
|
+
const emitSys = (event, payload)=>{
|
|
32
|
+
if (typeof event !== 'string' || !event) throw new Error('helper.emitSys: event must be a non-empty string');
|
|
33
|
+
if (typeof process.send !== 'function') throw new Error('helper.emitSys: system events are only available in worker processes');
|
|
34
|
+
process.send({__sysEvent: true, event, payload});
|
|
35
|
+
};
|
|
36
|
+
//监听广播事件, 每个 event 每个 worker 进程只保留一个 handler(重复注册覆盖),
|
|
37
|
+
//handler 签名 (payload, meta), meta 含 fromWorkerId/fromWorkerPid
|
|
38
|
+
const setListener = (event, handler)=>{
|
|
39
|
+
if (typeof event !== 'string' || !event) throw new Error('helper.setListener: event must be a non-empty string');
|
|
40
|
+
if (typeof handler !== 'function') throw new Error('helper.setListener: handler must be a function');
|
|
41
|
+
listeners[event] = handler;
|
|
42
|
+
};
|
|
43
|
+
//内部方法: 供 subWorker 收到 master 中转的广播时调用, 按 event 分发
|
|
44
|
+
const __dispatchBroadcast = (message)=>{
|
|
45
|
+
let {event, payload, meta} = message;
|
|
46
|
+
let handler = listeners[event];
|
|
47
|
+
if (!handler) return;
|
|
48
|
+
try{
|
|
49
|
+
handler(payload, meta);
|
|
50
|
+
}catch(e){
|
|
51
|
+
console.error(`[Broadcast] handler for event "${event}" threw:`, e);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
module.exports = {
|
|
55
|
+
load,
|
|
56
|
+
createNewTasks,
|
|
57
|
+
emit,
|
|
58
|
+
emitSys,
|
|
59
|
+
setListener,
|
|
60
|
+
__dispatchBroadcast
|
|
61
|
+
};
|