multi-tasks 3.0.4 → 3.1.0
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 +52 -2
- 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
|
@@ -17,17 +17,21 @@ npm install multi-tasks
|
|
|
17
17
|
- `multiTasks.resume(config)` — resume an interrupted run.
|
|
18
18
|
- `multiTasks.restart(config)` — resume and also retry failed tasks (needs `config.taskFolder`).
|
|
19
19
|
- `helper.createNewTasks(tasks)` — create new tasks dynamically while processing.
|
|
20
|
+
- `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.
|
|
21
|
+
- `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.
|
|
22
|
+
- `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`.
|
|
20
23
|
- Config options:
|
|
21
24
|
- `initialTasks` — array of task objects, or a folder path string to resume from.
|
|
22
25
|
- `processTask(task, helper)` — required; return a value or a Promise.
|
|
23
26
|
- `taskRootFolder` — root directory where progress and result files are stored.
|
|
24
27
|
- `taskId` — task folder name under `taskRootFolder`.
|
|
25
28
|
- `taskFolder` — full task folder path, required by `restart`.
|
|
26
|
-
- `numberOfWorkers` — how many worker processes run in parallel;
|
|
29
|
+
- `numberOfWorkers` — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
|
|
27
30
|
- `taskTimeout` — optional, milliseconds; an overdue task fails with a timeout error.
|
|
28
31
|
- `maxTaskRetries` — optional, max times a failed task is auto-retried.
|
|
29
32
|
- `autoCloseAfterCompletion` — set `false` if you create new tasks dynamically.
|
|
30
33
|
- `shouldTerminate(info)` — return `true` to terminate the whole process.
|
|
34
|
+
- `setSysListener(event, payload, meta)` — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
|
|
31
35
|
- `onFinish(report)` — called once after all workers are done.
|
|
32
36
|
|
|
33
37
|
### How to use:
|
|
@@ -138,6 +142,51 @@ multiTasks({
|
|
|
138
142
|
maxTaskRetries: 2,
|
|
139
143
|
});
|
|
140
144
|
|
|
145
|
+
//Example6, broadcast events between workers:
|
|
146
|
+
// a worker emits an event and the master relays it to every worker
|
|
147
|
+
// (best-effort, runtime only - not persisted, not replayed on
|
|
148
|
+
// resume); by default the sender also receives its own event, pass
|
|
149
|
+
// {includingMe: false} to exclude it; each event keeps only one
|
|
150
|
+
// listener per worker process, so calling setListener on every
|
|
151
|
+
// task is safe
|
|
152
|
+
multiTasks({
|
|
153
|
+
initialTasks: alltasks,
|
|
154
|
+
taskRootFolder: `../examples-tmp-data/example-broadcast`,
|
|
155
|
+
taskId: 'my-task',
|
|
156
|
+
numberOfWorkers: 3,
|
|
157
|
+
processTask: (task, helper) => {
|
|
158
|
+
helper.setListener('task-done', (payload, meta) => {
|
|
159
|
+
console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
|
|
160
|
+
});
|
|
161
|
+
helper.emit('task-done', {seq: task.seq});
|
|
162
|
+
return {data:'succ'};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
//Example7, system events from a worker to the master:
|
|
167
|
+
// helper.emitSys sends a system event that the master consumes
|
|
168
|
+
// itself (not relayed to workers); the built-in event
|
|
169
|
+
// TERMINATE_ALL_WORKERS force-kills all workers and exits the
|
|
170
|
+
// master (unfinished tasks stay for resume); any other event is
|
|
171
|
+
// passed to config.setSysListener on the master
|
|
172
|
+
multiTasks({
|
|
173
|
+
initialTasks: alltasks,
|
|
174
|
+
taskRootFolder: `../examples-tmp-data/example-sysevent`,
|
|
175
|
+
taskId: 'my-task',
|
|
176
|
+
numberOfWorkers: 3,
|
|
177
|
+
setSysListener: (event, payload, meta) => {
|
|
178
|
+
console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
|
|
179
|
+
},
|
|
180
|
+
processTask: (task, helper) => {
|
|
181
|
+
helper.emitSys('task-started', {seq: task.seq});
|
|
182
|
+
if(task.fatal){
|
|
183
|
+
helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
|
|
184
|
+
return;
|
|
185
|
+
};
|
|
186
|
+
return {data:'succ'};
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
141
190
|
```
|
|
142
191
|
|
|
143
192
|
### Resuming
|
|
@@ -161,6 +210,7 @@ The usage patterns documented above are covered by automated tests (unit tests i
|
|
|
161
210
|
|
|
162
211
|
### Changelog:
|
|
163
212
|
|
|
213
|
+
- 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
214
|
- 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
|
|
165
215
|
- 3.0.3 Support 'maxTaskRetries'
|
|
166
216
|
- 3.0.2 Support 'taskTimeout'
|
|
@@ -203,4 +253,4 @@ The usage patterns documented above are covered by automated tests (unit tests i
|
|
|
203
253
|
|
|
204
254
|
### License:
|
|
205
255
|
|
|
206
|
-
[MIT](https://opensource.org/
|
|
256
|
+
[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
|
+
};
|