multi-tasks 3.0.3 → 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 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
- - `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`.
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,8 @@ 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)
214
+ - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
164
215
  - 3.0.3 Support 'maxTaskRetries'
165
216
  - 3.0.2 Support 'taskTimeout'
166
217
  - 3.0.1 Versions 3.0.0 and above are maintained by AI
@@ -199,3 +250,7 @@ The usage patterns documented above are covered by automated tests (unit tests i
199
250
  ### Gitee:
200
251
 
201
252
  [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
253
+
254
+ ### License:
255
+
256
+ [MIT](https://opensource.org/license/MIT) (see [LICENSE](LICENSE))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
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": [
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const numCPUs = require('os').cpus().length;
4
+
5
+ const PERCENT_PATTERN = /^(\d+(?:\.\d+)?)%$/;
6
+
7
+ // 解析 numberOfWorkers 配置:
8
+ // - undefined -> 默认 "50%", 按核数四舍五入折算, 最小为 1;
9
+ // - number -> 原样返回(现状行为不变);
10
+ // - string -> 必须是 "数字%" 形式且百分比在 1-100 之间, 按核数四舍五入折算, 最小为 1;
11
+ // - 其余 -> 抛 Error。
12
+ const percentToWorkers = (percent) => {
13
+ return Math.max(1, Math.round(numCPUs * percent / 100));
14
+ };
15
+
16
+ const resolveNumberOfWorkers = (value) => {
17
+ if (typeof value === 'undefined') {
18
+ return percentToWorkers(50);
19
+ };
20
+ if (typeof value === 'number') {
21
+ return value;
22
+ };
23
+ if (typeof value === 'string') {
24
+ const match = value.trim().match(PERCENT_PATTERN);
25
+ if (match) {
26
+ const percent = parseFloat(match[1]);
27
+ if (percent >= 1 && percent <= 100) {
28
+ return percentToWorkers(percent);
29
+ };
30
+ };
31
+ };
32
+ throw new Error(`Invalid numberOfWorkers: ${JSON.stringify(value)}. Expected a number or a percentage string like "50%" (1-100).`);
33
+ };
34
+
35
+ module.exports = { resolveNumberOfWorkers };
@@ -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}`);
@@ -120,6 +122,37 @@ const start = (config)=>{
120
122
  let initWorker = (worker)=>{
121
123
  WorkerMgr.addWorker(worker);
122
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
+ }
123
156
  if(message === MSG_REQUEST_NEW_TASK){
124
157
  clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
125
158
  queue.enqueue(async ()=>{
@@ -213,6 +246,10 @@ const start = (config)=>{
213
246
  function subWorker(){
214
247
  let worker = cluster.worker;
215
248
  worker.on('message', function(message) {
249
+ if(message && message.__broadcast){//master 中转来的广播: 分发给 helper.on 注册的 handler
250
+ helper.__dispatchBroadcast(message);
251
+ return;
252
+ }
216
253
  if(message === MSG_NO_TASK_FOUND){//wait for new task
217
254
  console.log(` (Worker #${worker.id}): waiting`);
218
255
  setTimeout(()=>{
@@ -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;
@@ -60,7 +60,7 @@ const init = (config)=>{
60
60
  if(!config.initialTasks){ throw new Error('Please provide initialTasks data!'); };
61
61
  if(!config.processTask){ throw new Error('Please provide processTask function!'); };
62
62
 
63
- if(typeof config.numberOfWorkers === 'undefined') config.numberOfWorkers = numCPUs - 1;
63
+ config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
64
64
 
65
65
  let {taskRootFolder, taskName} = config;
66
66
 
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
- const load = (config)=>{
8
- myconfig = config;
9
- };
10
- const createNewTasks = (tasks)=>{
11
- if (!myconfig) throw new Error('Please call helper.load(config) first!');
12
- if (!Array.isArray(tasks)) tasks = [tasks];
13
- TaskMgr.load(myconfig);
14
- tasks.forEach((task)=>{
15
- console.log('Creating new task', task)
16
- TaskMgr.createNewTask(task);
17
- });
18
- };
19
- module.exports = {
20
- load,
21
- createNewTasks
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
+ };