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 CHANGED
@@ -12,23 +12,36 @@ npm install multi-tasks
12
12
 
13
13
  ### API:
14
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; also accepts a percentage string of CPU cores, e.g. `"50%"`.
27
- - `taskTimeout`optional, milliseconds; an overdue task fails with a timeout error.
28
- - `maxTaskRetries`optional, max times a failed task is auto-retried.
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.
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/licenses/MIT) (see [LICENSE](LICENSE))
266
+ [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.4",
3
+ "version": "3.1.1",
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": [
@@ -5,13 +5,17 @@ const numCPUs = require('os').cpus().length;
5
5
  const PERCENT_PATTERN = /^(\d+(?:\.\d+)?)%$/;
6
6
 
7
7
  // 解析 numberOfWorkers 配置:
8
- // - undefined -> CPU 核数减 1(默认);
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 numCPUs - 1;
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 Math.max(1, Math.round(numCPUs * percent / 100));
28
+ return percentToWorkers(percent);
25
29
  };
26
30
  };
27
31
  };
@@ -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
- 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
+ };