multi-tasks 3.2.1 → 3.2.3

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
@@ -1,8 +1,6 @@
1
1
  # multi-tasks
2
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`.
3
+ Multi-tasks is a toolkit to manage long-term and large-scale parallel computing tasks, with zero dependencies. Progress and tasks are stored on the file system, so tasks can be resumed even if the host crashes.
6
4
 
7
5
  ### Install:
8
6
 
@@ -18,13 +16,17 @@ Tip: monitor running tasks in real time — see [Real-Time Monitoring](#real-tim
18
16
  //see examples/example0
19
17
  let multiTasks = require('multi-tasks').multiTasks;
20
18
 
21
- //Step1, create the tasks to run in parallel as an array.
22
- let alltasks = [];
23
- for(let i=0;i<50;i++){
24
- alltasks.push({
25
- name: `task-${i}`,
26
- data: `This prop is for a subtask`
27
- });
19
+ //Step1, provide the tasks as a function returning them (or a Promise of them);
20
+ //it runs only once, in the master process.
21
+ let initialTasks = async ()=>{
22
+ let alltasks = [];
23
+ for(let i=0;i<50;i++){
24
+ alltasks.push({
25
+ name: `task-${i}`,
26
+ data: `This prop is for a subtask`
27
+ });
28
+ };
29
+ return alltasks;
28
30
  };
29
31
 
30
32
  //Step2, provide a function that processes each sub-task and returns the result
@@ -39,13 +41,14 @@ let processTask = (task, helper)=>{
39
41
 
40
42
  //Step3, run!
41
43
  multiTasks({
42
- initialTasks: alltasks,
44
+ initialTasks, //accepts array, function, or function returning a promise
43
45
  processTask,
44
46
  taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and result files; you can check the progress here
45
47
  taskId: 'my-task',
46
48
  numberOfWorkers: 3, //how many workers are working in parallel
47
49
  //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
48
50
  //maxTaskRetries: 2, //optional, auto-retry a failed task (worker crash, processTask error, or timeout); retried tasks go back to the queue
51
+ //workerPriority: 'low', //optional, one of 'low' | 'below_normal' | 'normal' | 'above_normal' | 'high' (or a nice number -20~19); lower the OS scheduling priority of workers to keep the machine responsive during long batches
49
52
  //autoCloseAfterCompletion: true, //set true to exit the process when all tasks are done; keep it false (default) if you create new tasks dynamically
50
53
  shouldTerminate:(info)=>{
51
54
  //return true if you need to terminate the whole process
@@ -175,6 +178,27 @@ multiTasks({
175
178
 
176
179
  ```
177
180
 
181
+ ### Performance: runs only once, in the master process
182
+
183
+ Every worker process re-runs your entire entry script — so top-level task-generation code (building a huge array, scanning folders, querying a database, ...) runs once per worker. Wrap it in a function for performance: it is called **only once, in the master process**, and workers never call it. `initialTasks` accepts three forms — pick one:
184
+
185
+ ```javascript
186
+
187
+ //Example8, the three forms of initialTasks
188
+
189
+ //form 1: a plain array — assign it directly
190
+ multiTasks({ initialTasks: alltasks, processTask, taskRootFolder, taskId });
191
+
192
+ //form 2: a function returning the array
193
+ multiTasks({ initialTasks: () => fetchTasksFromSomewhere(), processTask, taskRootFolder, taskId });
194
+
195
+ //form 3: a function returning a Promise of the array
196
+ multiTasks({ initialTasks: async () => await fetchTasksFromDb(), processTask, taskRootFolder, taskId });
197
+
198
+ ```
199
+
200
+ The (resolved) value must be an array, otherwise the master exits with an error. The resulting array — not the function — is stored in `task_config.json`, so the self-contained-function note under Resuming does not apply to it.
201
+
178
202
  ### Resuming
179
203
 
180
204
  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:
@@ -261,13 +285,14 @@ The API has three parts: the entry functions, the config options, and the `helpe
261
285
 
262
286
  **Config options:**
263
287
 
264
- - **`initialTasks`** — array of task objects, or a folder path string to resume from.
288
+ - **`initialTasks`** — array of task objects, a function returning the array (or a Promise of it — called only once, in the master process; worker processes never call it; see [Performance: runs only once, in the master process](#performance-runs-only-once-in-the-master-process)), or a folder path string to resume from.
265
289
  - **`processTask(task, helper)`** — required; return a value or a Promise.
266
290
  - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
267
291
  - **`taskId`** — task folder name under `taskRootFolder`.
268
292
  - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
269
293
  - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
270
294
  - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
295
+ - **`workerPriority`** — optional, OS scheduling priority for worker processes: `'low' | 'below_normal' | 'normal' | 'above_normal' | 'high'`, or a nice number (integer -20~19). Lower priority keeps the machine responsive while a long batch occupies all cores; invalid values throw before any task folder is created.
271
296
  - **`progressBar`** — `true` by default: shows a live single-line progress bar in the terminal (set `false` to disable); logs (including your own `console.*` calls) always go to `<taskFolder>/.sys/run.log`, and print to the console as well only when the bar is off or stdout is not a TTY.
272
297
  - **`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.
273
298
  - **`shouldTerminate(info)`** — return `true` to terminate the whole process.
@@ -290,6 +315,8 @@ The API has three parts: the entry functions, the config options, and the `helpe
290
315
 
291
316
  ### Changelog:
292
317
 
318
+ - 3.2.3 Support workerPriority: set OS scheduling priority for worker processes
319
+ - 3.2.2 initialTasks accepts a function (sync or async) that is called only once, in the master process
293
320
  - 3.2.1 Fix retry bug: user-defined taskCount/index no longer overwritten on task retries
294
321
  - 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
295
322
  - 3.1.9 Print the multi-tasks-monitor command on startup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
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,37 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+
5
+ const PRIORITY_NAMES = {
6
+ low: os.constants.priority.PRIORITY_LOW,
7
+ below_normal: os.constants.priority.PRIORITY_BELOW_NORMAL,
8
+ normal: os.constants.priority.PRIORITY_NORMAL,
9
+ above_normal: os.constants.priority.PRIORITY_ABOVE_NORMAL,
10
+ high: os.constants.priority.PRIORITY_HIGH,
11
+ };
12
+
13
+ // 解析 workerPriority 配置:
14
+ // - undefined -> 返回 undefined(不设置优先级, 行为不变);
15
+ // - string -> 枚举 'low'|'below_normal'|'normal'|'above_normal'|'high'(允许首尾空白),
16
+ // 映射 os.constants.priority 常量;
17
+ // - number -> 须为 -20~19 的整数(os.setPriority 原生 nice 范围), 原样返回;
18
+ // - 其余 -> 抛 Error。
19
+ const resolveWorkerPriority = (value) => {
20
+ if (typeof value === 'undefined') {
21
+ return undefined;
22
+ };
23
+ if (typeof value === 'string') {
24
+ const name = value.trim();
25
+ if (Object.prototype.hasOwnProperty.call(PRIORITY_NAMES, name)) {
26
+ return PRIORITY_NAMES[name];
27
+ };
28
+ };
29
+ if (typeof value === 'number') {
30
+ if (Number.isInteger(value) && value >= -20 && value <= 19) {
31
+ return value;
32
+ };
33
+ };
34
+ throw new Error(`Invalid workerPriority: ${JSON.stringify(value)}. Expected 'low' | 'below_normal' | 'normal' | 'above_normal' | 'high', or an integer between -20 and 19.`);
35
+ };
36
+
37
+ module.exports = { resolveWorkerPriority };
@@ -1,5 +1,6 @@
1
1
  const cluster = require('cluster');
2
- const numCPUs = require('os').cpus().length;
2
+ const os = require('os');
3
+ const numCPUs = os.cpus().length;
3
4
 
4
5
  let MSG_REQUEST_NEW_TASK = 'request_new_task';
5
6
  let MSG_NO_TASK_FOUND = 'no_task_found';
@@ -9,6 +10,7 @@ const TaskMgr = require('./TaskMgr');
9
10
  const WorkerMgr = require('./WorkerMgr');
10
11
  const helper = require('./helper');
11
12
  const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
13
+ const { resolveWorkerPriority } = require('../utils/resolvePriority');
12
14
  const runLog = require('../utils/runLog');
13
15
  const progressCtl = require('../utils/progressCtl');
14
16
  const startupBanner = require('../utils/startupBanner');
@@ -57,6 +59,7 @@ const start = (config)=>{
57
59
  progressCtl.reset();
58
60
  runLog.reset();//runLog 已脱离 progressCtl: 不复位则新一轮日志继续写旧 run.log、console 仍被旧 hook 拦截
59
61
  config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串; 须在 fork 循环读值前折算, 非法值在此抛错
62
+ config.workerPriority = resolveWorkerPriority(config.workerPriority);//枚举映射/数字校验; 非法值 fail fast(同 numberOfWorkers)
60
63
  let numOfWorkers = config.numberOfWorkers;
61
64
 
62
65
  const AsyncQueue = require(`../utils/asyncQueue`);
@@ -126,6 +129,14 @@ const start = (config)=>{
126
129
 
127
130
  let initWorker = (worker)=>{
128
131
  WorkerMgr.addWorker(worker);
132
+ if (typeof config.workerPriority === 'number') {
133
+ try {
134
+ os.setPriority(worker.process.pid, config.workerPriority);
135
+ } catch (e) {
136
+ //优先级是建议性设置: 失败(如 POSIX 上调高需 root)仅告警, 不中断批任务
137
+ runLog.warn(`[Master]: failed to set priority of worker pid=${worker.process.pid}`, e);
138
+ };
139
+ };
129
140
  worker.on('message', function(message) {
130
141
  if(message && message.__sysEvent){//worker 系统事件: master 自己消费, 不转发
131
142
  if(message.event === 'TERMINATE_ALL_WORKERS'){
package/workers/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const pathUtil = require('path');
3
+ const cluster = require('cluster');
3
4
  const Multithread = require('./Multithread');
4
5
  const backwardv1 = require('../utils/backward-v1');
5
6
  const { FUNCTION_CONFIG_KEYS, reviveConfig } = require('../utils/serializeFns');
@@ -52,19 +53,58 @@ const _computeSysLogFile = (config)=>{
52
53
  };
53
54
  };
54
55
 
56
+ //initialTasks 支持函数形式(惰性生成): 只在 master 进程求值一次, worker 进程重跑入口脚本时跳过,
57
+ //防生成逻辑(扫目录/查库等初始化脚本)在每个 worker 里各执行一遍。
58
+ //函数可同步返回数组, 也可返回 Promise(resolve 值同样必须是数组)——同步路径保持同步 throw,
59
+ //Promise 路径返回 then 链交给 _proceedAfterEval 等待。求值在入口统一完成(先于 _mergeSnapshotConfig
60
+ //与 Multithread.start): 求值后的数组才会参与合并并写入 task_config.json(函数本身会被
61
+ //JSON.stringify 丢弃, 不进存档、不参与 revive)。
62
+ const _evalInitialTasks = (config)=>{
63
+ if(typeof config.initialTasks !== 'function') return null;
64
+ if(!(cluster.isPrimary || cluster.isMaster)) return null;
65
+ let tasks = config.initialTasks();
66
+ let applyTasks = (resolved)=>{
67
+ if(!Array.isArray(resolved)){
68
+ throw new Error('FATAL: initialTasks function should return an array!');
69
+ };
70
+ config.initialTasks = resolved;
71
+ };
72
+ if(tasks && typeof tasks.then === 'function'){//Promise 形式: 异步求值, resolve 后同样校验数组(假设真 Promise, 即 .then 返回 Promise; 非标准 thenable 不支持)
73
+ return tasks.then(applyTasks);
74
+ };
75
+ applyTasks(tasks);
76
+ return null;
77
+ };
78
+
79
+ //同步路径(pending 为 null)直接执行 proceed, 保持既有同步 throw 语义;
80
+ //Promise 路径等 resolve 后执行, resolve 非数组/reject/proceed 抛错时打 FATAL 并 exit(1)
81
+ //(与 init 异步失败一致; Promise 路径只发生在 master, 无同步 throw 对象)。
82
+ const _proceedAfterEval = (pending, proceed)=>{
83
+ if(!pending){
84
+ proceed();
85
+ return;
86
+ };
87
+ return pending.then(proceed).catch((e)=>{
88
+ console.error('[FATAL] failed to evaluate initialTasks:', e);
89
+ process.exit(1);
90
+ });
91
+ };
92
+
55
93
  const start = (config)=>{
56
94
  config = backwardv1.v1tov2(config);
95
+ return _proceedAfterEval(_evalInitialTasks(config), ()=>{
57
96
 
58
- config.taskRootFolder = _toAbsPath(config.taskRootFolder);
59
- config.taskFolder = _toAbsPath(config.taskFolder);
60
- _computeSysLogFile(config);
97
+ config.taskRootFolder = _toAbsPath(config.taskRootFolder);
98
+ config.taskFolder = _toAbsPath(config.taskFolder);
99
+ _computeSysLogFile(config);
61
100
 
62
- Multithread.start(config);
101
+ Multithread.start(config);
102
+ });
63
103
  };
64
104
  const resume = (config)=>{
65
105
  config = _normalizeResumeArg('resume', config);
66
- config.__resume = true;
67
-
106
+ //先校验/推导 taskFolder 再求值 initialTasks(与 retry_fails/restart 一致):
107
+ //缺目录时同步 throw fail fast, 不让可能昂贵的生成逻辑白跑一遍
68
108
  config.taskRootFolder = _toAbsPath(config.taskRootFolder);
69
109
  config.taskFolder = _toAbsPath(config.taskFolder);
70
110
  if(!config.taskFolder){
@@ -75,10 +115,12 @@ const resume = (config)=>{
75
115
  };
76
116
  config.taskRootFolder = pathUtil.dirname(config.taskFolder);
77
117
  config.taskId = pathUtil.basename(config.taskFolder);
78
- _computeSysLogFile(config);
79
-
80
- config = _mergeSnapshotConfig(config);
81
- Multithread.start(config);
118
+ config.__resume = true;
119
+ return _proceedAfterEval(_evalInitialTasks(config), ()=>{
120
+ _computeSysLogFile(config);
121
+ config = _mergeSnapshotConfig(config);
122
+ Multithread.start(config);
123
+ });
82
124
  };
83
125
  const retry_fails = (config)=>{
84
126
  config = _normalizeResumeArg('retry_fails', config);
@@ -91,9 +133,11 @@ const retry_fails = (config)=>{
91
133
  config.taskId = pathUtil.basename(taskFolder);
92
134
  config.__resume = true;
93
135
  config.__retryFails = true;
94
- _computeSysLogFile(config);
95
- config = _mergeSnapshotConfig(config);
96
- Multithread.start(config);
136
+ return _proceedAfterEval(_evalInitialTasks(config), ()=>{
137
+ _computeSysLogFile(config);
138
+ config = _mergeSnapshotConfig(config);
139
+ Multithread.start(config);
140
+ });
97
141
  };
98
142
  const restart = (config)=>{
99
143
  config = _normalizeResumeArg('restart', config);
@@ -105,9 +149,11 @@ const restart = (config)=>{
105
149
  config.taskRootFolder = pathUtil.dirname(taskFolder);
106
150
  config.taskId = pathUtil.basename(taskFolder);
107
151
  config.__restart = true;
108
- _computeSysLogFile(config);
109
- config = _mergeSnapshotConfig(config);
110
- Multithread.start(config);
152
+ return _proceedAfterEval(_evalInitialTasks(config), ()=>{
153
+ _computeSysLogFile(config);
154
+ config = _mergeSnapshotConfig(config);
155
+ Multithread.start(config);
156
+ });
111
157
  };
112
158
 
113
159
  const multiTasks = (config)=>{
@@ -126,7 +172,7 @@ const multiTasks = (config)=>{
126
172
  config.taskRootFolder = pathUtil.dirname(folder);
127
173
  }
128
174
  }
129
- if(!isResume) throw new Error('FATAL: initialTasks should be an array or a task root folder!');
175
+ if(!isResume) throw new Error('FATAL: initialTasks should be an array, a function or a task root folder!');
130
176
  }else{
131
177
  let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
132
178
  if(fs.existsSync(taskFolder)){
@@ -135,9 +181,9 @@ const multiTasks = (config)=>{
135
181
  }
136
182
 
137
183
  if(isResume){
138
- resume(config);
184
+ return resume(config);
139
185
  }else{
140
- start(config);
186
+ return start(config);
141
187
  }
142
188
 
143
189
  };