multi-tasks 3.3.0 → 3.4.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
@@ -1,375 +1,277 @@
1
- # multi-tasks
2
-
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.
4
-
5
- ### Install:
6
-
7
- ```javascript
8
- npm install multi-tasks
9
- ```
10
-
11
- Tip: monitor running tasks in real time — see [Real-Time Monitoring](#real-time-monitoring).
12
-
13
- ### How to use:
14
-
15
- ```javascript
16
- //see examples/example0
17
- let multiTasks = require('multi-tasks').multiTasks;
18
-
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;
30
- };
31
-
32
- //Step2, provide a function that processes each sub-task and returns the result
33
- let processTask = (task, helper)=>{
34
- let {taskCount} = task;//get your task data
35
-
36
- //Run your processing logic here...
37
-
38
- //then return the result as a plain javascript object or an async promise
39
- return 'result data';
40
- };
41
-
42
- //Step3, run!
43
- multiTasks({
44
- initialTasks, //accepts array, function, or function returning a promise
45
- processTask,
46
- taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and result files; you can check the progress here
47
- taskId: 'my-task',
48
- numberOfWorkers: 3, //how many workers are working in parallel
49
- //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
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
52
- //autoCloseAfterCompletion: true, //set true to exit the process when all tasks are done; keep it false (default) if you create new tasks dynamically
53
- shouldTerminate:(info)=>{
54
- //return true if you need to terminate the whole process
55
- },
56
- onFinish: (report, helper)=>{
57
- console.log('finish callback', report);
58
- helper.foreachSuccResult((count, task, result)=>{
59
- console.log(`#${count} succeeded:`, task.name, result);
60
- });
61
- helper.foreachErrorResult((count, task, err)=>{
62
- console.log(`#${count} failed:`, task.name, err);
63
- });
64
- }
65
- });
66
-
67
- ```
68
-
69
- ### More about the task processing function
70
-
71
- ```javascript
72
- //Examples 1-3 below are independent variants of processTask, use one at a time.
73
-
74
- //Example1, return a promise for async processes,
75
- // you can return a promise or a non-promise result,
76
- // all result data can be found in the results/succ folder
77
- let processTask = (task, helper)=>{
78
- let {taskCount} = task;
79
-
80
- return new Promise((resolve, reject)=>{
81
- resolve({
82
- data:`task${taskCount} complete`
83
- })
84
- })
85
- };
86
-
87
- //Example2, dynamically create a new task while processing
88
- let processTask = (task, helper)=>{
89
- let {taskCount} = task;
90
-
91
- if(taskCount % 2 === 0){
92
- //create a new task if needed
93
- helper.createNewTasks({
94
- msg:'a new task'
95
- });
96
- return;
97
- }
98
-
99
- return new Promise((resolve, reject)=>{
100
- resolve({
101
- data:`task${taskCount} complete`
102
- })
103
- })
104
- };
105
-
106
- //Example3, generate/throw exceptions in a task method
107
- let processTask = (task, helper)=>{
108
- let {taskCount} = task;
109
-
110
- //Demo of exceptions/errors: they are captured and saved in the results/errors folder
111
- if(taskCount===3) throw 'exception';
112
- if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
113
- if(taskCount===5) aaa = bbb;//this ReferenceError will be captured by multi-tasks
114
-
115
- return {data:'succ'};
116
- }
117
-
118
- //Example4, timeout: a task that does not finish within taskTimeout is
119
- // treated as failed with a timeout error
120
- multiTasks({
121
- initialTasks: alltasks,
122
- processTask,
123
- taskRootFolder: `../examples-tmp-data/example-timeout`,
124
- taskId: 'my-task',
125
- numberOfWorkers: 3,
126
- taskTimeout: 30000,
127
- });
128
-
129
- //Example5, auto-retry a failed task with maxTaskRetries
130
- multiTasks({
131
- initialTasks: alltasks,
132
- processTask,
133
- taskRootFolder: `../examples-tmp-data/example-retry`,
134
- taskId: 'my-task',
135
- numberOfWorkers: 3,
136
- maxTaskRetries: 2,
137
- });
138
-
139
- ```
140
-
141
- ### Event broadcast
142
-
143
- ```javascript
144
-
145
- //Example6, broadcast events between workers with helper.emit/helper.setListener
146
- multiTasks({
147
- initialTasks: alltasks,
148
- taskRootFolder: `../examples-tmp-data/example-broadcast`,
149
- taskId: 'my-task',
150
- numberOfWorkers: 3,
151
- processTask: (task, helper) => {
152
- helper.setListener('task-done', (payload, meta) => {
153
- console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
154
- });
155
- helper.emit('task-done', {seq: task.seq});
156
- return {data:'succ'};
157
- },
158
- });
159
-
160
- //Example7, system events from a worker to the master with helper.emitSys
161
- multiTasks({
162
- initialTasks: alltasks,
163
- taskRootFolder: `../examples-tmp-data/example-sysevent`,
164
- taskId: 'my-task',
165
- numberOfWorkers: 3,
166
- setSysListener: (event, payload, meta) => {
167
- console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
168
- },
169
- processTask: (task, helper) => {
170
- helper.emitSys('task-started', {seq: task.seq});
171
- if(task.fatal){
172
- helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
173
- return;
174
- };
175
- return {data:'succ'};
176
- },
177
- });
178
-
179
- ```
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
-
202
- ### Resuming
203
-
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:
205
-
206
- ```javascript
207
-
208
- //same command, same config as the interrupted run — that's all
209
- multiTasks(config);
210
-
211
- ```
212
-
213
- Or point `initialTasks` at the task folder same effect:
214
-
215
- ```javascript
216
-
217
- multiTasks({ ...config, initialTasks: `/myworks/my_scan_tasks/my-task` });
218
-
219
- ```
220
-
221
- Or resume from the task folder alone the config (including `processTask`) is restored from `task_config.json`:
222
-
223
- ```javascript
224
-
225
- multiTasks.resume({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
226
-
227
- ```
228
-
229
- **Note:** revived functions must be **self-contained** (no outer-scope variables; `require(...)` inside the body) otherwise pass them again, they always win:
230
-
231
- ```javascript
232
- multiTasks.resume({
233
- taskFolder: `/myworks/my_scan_tasks/my-task`,
234
- processTask,
235
- });
236
- ```
237
-
238
- ### Retrying failed tasks
239
-
240
- To resume an interrupted run and also retry the tasks that failed (in task folder `progress/finished_with_errors`):
241
-
242
- ```javascript
243
-
244
- multiTasks.retry_fails({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
245
-
246
- ```
247
-
248
- ### Restarting from scratch
249
-
250
- To throw away all progress and results in a task folder and re-run all the initial tasks from the beginning (the config, including `processTask` and `initialTasks`, is restored from `task_config.json`):
251
-
252
- ```javascript
253
-
254
- multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes all previous progress and results, then re-runs all the initial tasks
255
-
256
- ```
257
-
258
- Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **all the initial tasks** — tasks created dynamically via `helper.createNewTasks` are not restored and will not be re-run. Fields you pass (e.g. `processTask`) override the snapshot and are written back into it; the same self-contained-function note as in Resuming applies.
259
-
260
- ### Real-Time Monitoring
261
-
262
- Monitor your running tasks with [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) see its npm page for full details.
263
-
264
- Quick start (requires multi-tasks >=3.2.0): once your multi-tasks run has started, the startup log prints the monitor command with the task folder path already filled in — just copy-paste it into another terminal:
265
-
266
- ```text
267
- ****************************************************************
268
- [Master]: monitor this task with:
269
- npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
270
- ****************************************************************
271
- ```
272
-
273
- For installation, command-line options, and more, see the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
274
-
275
- ### API:
276
-
277
- The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
278
-
279
- **Entry functions:**
280
-
281
- - **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
282
- - **`multiTasks.resume(config)`** — usually you don't need this: the simplest way to resume is to just run `multiTasks(config)` again, it auto-resumes when the task folder already exists. This method resumes an interrupted run from the task folder alone; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
283
- - **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
284
- - **`multiTasks.restart(config)`** — wipe the task folder's progress and re-run from scratch; pass a config object with `taskFolder`. The config (including `processTask` and `initialTasks`) is restored from `task_config.json`; any field you pass overrides the snapshot, and overrides are written back into it. **All previous progress and results are deleted.**
285
-
286
- **Config options:**
287
-
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.
289
- - **`processTask(task, helper)`** — required; return a result or a Promise with result. Results are stored as JSON in `results/succ/<subid>.json`; if a result cannot be saved because it is not JSON-serializable, the task still counts as succeeded, and a warning shows up in the command line and in monitor tools.
290
- - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
291
- - **`taskId`** — task folder name under `taskRootFolder`.
292
- - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
293
- - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
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.
296
- - **`progressBar`** — `true` by default: shows a live progress bar in the terminal; set `false` to fall back to plain log output.
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.
298
- - **`shouldTerminate(info)`** — return `true` to terminate the whole process. If it throws, the error is logged and the task is dispatched anyway (treated as "don't terminate").
299
- - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
300
- - **`onFinish(report, helper)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, succButSaveFailed, workers }` — `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `succButSaveFailed` counts tasks that ran fine but whose results could not be JSON-serialized (details in `<taskFolder>/.sys/user_messages/`); `workers` is `{ count, crashed }` — the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
301
- - **`helper.foreachSuccResult((count, task, result) => {})`** — iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing; a `{type: 'result_serialize_error', message}` placeholder when the returned value could not be JSON-serialized).
302
- - **`helper.foreachErrorResult((count, task, err) => {})`** — iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`). A non-`Error` rejection value that is not JSON-serializable is persisted as `{ex: {type: 'unserializable_error_value', ...}}`.
303
- - Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns — don't `await` inside it.
304
-
305
- **Other options:**
306
-
307
- - **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`/`restart`.
308
-
309
- **Task helper** (the `helper` object passed as the second argument of `processTask`):
310
-
311
- - **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
312
- - **`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.
313
- - **`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.
314
- - **`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`.
315
-
316
- ### Changelog:
317
-
318
- - 3.3.0 Unserializable results still succeed; shouldTerminate throwing is safe; progressBar counters added
319
- - 3.2.3 Support workerPriority: set OS scheduling priority for worker processes
320
- - 3.2.2 initialTasks accepts a function (sync or async) that is called only once, in the master process
321
- - 3.2.1 Fix retry bug: user-defined taskCount/index no longer overwritten on task retries
322
- - 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
323
- - 3.1.9 Print the multi-tasks-monitor command on startup
324
- - 3.1.8 Small refinements
325
- - 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
326
- - 3.1.6 Fix silent task loss under heavy load with maxTaskRetries
327
- - 3.1.5 Update README
328
- - 3.1.4 Rewrite methods multiTasks.resume/retry_fails/restart
329
- - 3.1.3 Update README
330
- - 3.1.2 Update README
331
- - 3.1.1 Fix readme documentation
332
- - 3.1.0 Support worker broadcast and system events; default numberOfWorkers is now "50%" of CPU cores
333
- - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
334
- - 3.0.3 Support 'maxTaskRetries'
335
- - 3.0.2 Support 'taskTimeout'
336
- - 3.0.1 Versions 3.0.0 and above are maintained by AI
337
- - 2.2.0 Support Resuming from an interrupted task
338
- - 2.1.2 Update readme
339
- - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
340
- - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
341
- - 2.0.9 Update readme examples
342
- - 2.0.8 Update readme examples
343
- - 2.0.7 Update changelog
344
- - 2.0.6 Update readme, remove failed examples
345
- - 2.0.5 Update readme examples
346
- - 2.0.4 Support resume from a failed task
347
- - 2.0.3 Fix: mkdir bug on windows
348
- - 2.0.2 new feature: support shouldTerminate
349
- - 2.0.1 Avoid possible I/O conflicts.
350
- - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
351
- - 1.2.8 Fix: create task folder failed on MacOS
352
- - 1.2.7 Small updates
353
- - 1.2.6 Support onFinish event
354
- - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
355
- - 1.2.4 Fix: opt.numberOfWorkers not work
356
- - 1.2.3 Update README
357
- - 1.2.2 Update README and examples
358
- - 1.2.1 Handle exceptions and errors in subtasks
359
- - 1.2.0 Simplified usage by providing the function way and support return Promise
360
- - 1.1.4 Remove make-dir
361
- - 1.1.3 Simplified usage, see example0
362
- - 1.1.2
363
- - 1.1.1 Rename files, updated changelog
364
- - 1.1.0 Simplified the usage of a customized Consumer, see example0
365
- - 1.0.8 Fix examples
366
- - 1.0.7 Remove moment
367
- - 1.0.6 Performance optimization
368
-
369
- ### Gitee:
370
-
371
- [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
372
-
373
- ### License:
374
-
375
- [MIT](https://opensource.org/license/MIT)
1
+ # multi-tasks
2
+
3
+ Run large batches of tasks in parallel across multiple Node.js processes, with zero dependencies. Progress and results are stored on the file system, so an interrupted run (crash, power outage) can resume where it left off.
4
+
5
+ ### Install:
6
+
7
+ ```text
8
+ npm install multi-tasks
9
+ ```
10
+
11
+ ### Quick start:
12
+
13
+ ```javascript
14
+ let multiTasks = require('multi-tasks').multiTasks;
15
+
16
+ //your tasks, as a plain array
17
+ let tasks = [];
18
+ for(let i = 0; i < 50; i++){
19
+ tasks.push({name: `task-${i}`});
20
+ };
21
+
22
+ multiTasks({
23
+ initialTasks: tasks,
24
+ //process one task and return its result (a plain value or a Promise)
25
+ processTask: (task, helper) => {
26
+ return `done: ${task.name}`;
27
+ },
28
+ taskRootFolder: './my-tasks', //progress and results live in ./my-tasks/demo
29
+ taskId: 'demo',
30
+ numberOfWorkers: 3, //worker processes running in parallel
31
+ autoCloseAfterCompletion: true, //exit the process when all tasks are done
32
+ onFinish: (report) => {
33
+ console.log(`${report.succeeded} succeeded, ${report.failed} failed`);
34
+ },
35
+ });
36
+ ```
37
+
38
+ Results are written as JSON files under `<taskRootFolder>/<taskId>/results/succ`, failures under `results/errors`. While running, a live progress bar shows in the terminal to watch the run from a browser, see [Real-Time Monitoring](#real-time-monitoring). A runnable copy of this example lives in `examples/example0`.
39
+
40
+ ### Results and errors:
41
+
42
+ `processTask` may return a plain value or a Promise:
43
+
44
+ ```javascript
45
+ let processTask = (task, helper) => {
46
+ let {taskCount} = task; //0-based index of this task
47
+
48
+ return new Promise((resolve, reject) => {
49
+ setTimeout(() => {
50
+ resolve({data: `task${taskCount} complete`});
51
+ }, 100);
52
+ });
53
+ };
54
+ ```
55
+
56
+ A failing task never stops the batch — its error is captured and saved to `results/errors`, the rest keep going:
57
+
58
+ ```javascript
59
+ let processTask = (task, helper) => {
60
+ let {taskCount} = task;
61
+
62
+ if(taskCount === 3) throw 'exception'; //a thrown value is persisted as-is
63
+ if(taskCount === 4) return Promise.reject({err: 'a test error'}); //a rejection value is persisted as-is
64
+ if(taskCount === 5) aaa = bbb; //a ReferenceError is captured too
65
+
66
+ return {data: 'succ'};
67
+ };
68
+ ```
69
+
70
+ ### Creating new tasks dynamically:
71
+
72
+ ```javascript
73
+ //scenario: crawl a paginated API — each page tells you if there is a next one
74
+ let processTask = async (task, helper) => {
75
+ let page = await fetchPage(task.url); //your logic here
76
+
77
+ if(page.nextUrl){
78
+ //a discovered next page becomes a new task;
79
+ //the pages are finite, so the queue always drains on its own
80
+ helper.createNewTasks({url: page.nextUrl});
81
+ };
82
+
83
+ return {url: task.url, items: page.items};
84
+ };
85
+
86
+ multiTasks({
87
+ initialTasks: [{url: 'https://api.example.com/items?page=1'}], //start from the first page
88
+ processTask,
89
+ taskRootFolder: './my-tasks',
90
+ taskId: 'api-crawl',
91
+ numberOfWorkers: 3,
92
+ //keep autoCloseAfterCompletion false (the default) while tasks may still be created
93
+ });
94
+ ```
95
+
96
+ The point: new tasks come from work discovered while processing, and discovery stops when the data runs out — no artificial bound needed. Keep `autoCloseAfterCompletion` at its default (`false`) so workers stay alive for late-arriving tasks.
97
+
98
+ ### Timeouts and retries:
99
+
100
+ ```javascript
101
+ multiTasks({
102
+ initialTasks: tasks,
103
+ processTask,
104
+ taskRootFolder: './my-tasks',
105
+ taskId: 'demo',
106
+ numberOfWorkers: 3,
107
+ taskTimeout: 30000, //a task that overruns 30s fails with a timeout error
108
+ maxTaskRetries: 2, //auto-retry a failed task (worker crash, processTask error, or timeout) up to 2 times
109
+ });
110
+ ```
111
+
112
+ ### Resuming an interrupted run:
113
+
114
+ Just re-run the same code — `multiTasks(config)` detects the existing task folder and auto-resumes, re-running only the stuck tasks:
115
+
116
+ ```javascript
117
+ multiTasks(config); //same command, same config as the interrupted run
118
+ ```
119
+
120
+ Or point `initialTasks` at the task folder — same effect:
121
+
122
+ ```javascript
123
+ multiTasks({...config, initialTasks: '/myworks/my_scan_tasks/my-task'});
124
+ ```
125
+
126
+ Or resume from the task folder alone — the config (including `processTask`) is restored from `task_config.json`:
127
+
128
+ ```javascript
129
+ multiTasks.resume({taskFolder: '/myworks/my_scan_tasks/my-task'}); //taskFolder: the full path of the existing task folder
130
+ ```
131
+
132
+ **Note:** functions revived from `task_config.json` must be **self-contained** (no outer-scope variables; `require(...)` inside the body) — otherwise pass them again; fields you pass always win:
133
+
134
+ ```javascript
135
+ multiTasks.resume({taskFolder: '/myworks/my_scan_tasks/my-task', processTask});
136
+ ```
137
+
138
+ ### Retrying failed tasks:
139
+
140
+ Resume an interrupted run and also re-run the tasks that failed (in `progress/finished_with_errors`):
141
+
142
+ ```javascript
143
+ multiTasks.retry_fails({taskFolder: '/myworks/my_scan_tasks/my-task'});
144
+ ```
145
+
146
+ ### Restarting from scratch:
147
+
148
+ Throw away all progress and results in a task folder and re-run all the initial tasks (the config, including `processTask` and `initialTasks`, is restored from `task_config.json`):
149
+
150
+ ```javascript
151
+ multiTasks.restart({taskFolder: '/myworks/my_scan_tasks/my-task'});
152
+ ```
153
+
154
+ Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **all the initial tasks** — tasks created dynamically via `helper.createNewTasks` are not restored and will not be re-run. Fields you pass override the saved config and are written back into it; the same self-contained-function note as in Resuming applies.
155
+
156
+ ### Broadcasting events between workers:
157
+
158
+ ```javascript
159
+ multiTasks({
160
+ initialTasks: tasks,
161
+ processTask: (task, helper) => {
162
+ helper.setListener('task-done', (payload, meta) => {
163
+ console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
164
+ });
165
+ helper.emit('task-done', {seq: task.seq}); //broadcast to all workers
166
+ return {data: 'succ'};
167
+ },
168
+ taskRootFolder: './my-tasks',
169
+ taskId: 'demo',
170
+ numberOfWorkers: 3,
171
+ });
172
+ ```
173
+
174
+ ### System events:
175
+
176
+ Send events from a worker to the master with `helper.emitSys`:
177
+
178
+ ```javascript
179
+ multiTasks({
180
+ initialTasks: tasks,
181
+ setSysListener: (event, payload, meta) => {
182
+ console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
183
+ },
184
+ processTask: (task, helper) => {
185
+ helper.emitSys('task-started', {seq: task.seq});
186
+ if(task.fatal){
187
+ helper.emitSys('TERMINATE_ALL_WORKERS'); //built-in event: stop everything
188
+ return;
189
+ };
190
+ return {data: 'succ'};
191
+ },
192
+ taskRootFolder: './my-tasks',
193
+ taskId: 'demo',
194
+ numberOfWorkers: 3,
195
+ });
196
+ ```
197
+
198
+ ### Real-Time Monitoring
199
+
200
+ Monitor your running tasks with [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) see its npm page for full details.
201
+
202
+ Quick start (requires multi-tasks >=3.2.0): once your multi-tasks run has started, the startup log prints the monitor command with the task folder path already filled in — just copy-paste it into another terminal:
203
+
204
+ ```text
205
+ npm install multi-tasks-monitor
206
+ npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
207
+ ```
208
+
209
+ ### API:
210
+
211
+ **Entry functions:**
212
+
213
+ - **`multiTasks(config)`** run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
214
+ - **`multiTasks.resume(config)`** — resume from the task folder alone (`{taskFolder}`); the config is restored from `task_config.json`, fields you pass override it and are written back. Usually you don't need this: re-running `multiTasks(config)` auto-resumes.
215
+ - **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
216
+ - **`multiTasks.restart(config)`** — wipe the task folder's progress and results and re-run from scratch (`{taskFolder}`). **All previous progress and results are deleted.**
217
+
218
+ **Config options:**
219
+
220
+ - **`initialTasks`** — an array of task objects; a function returning the array (or a Promise of it); or a task folder path string to resume from.
221
+ - Function form, for performance: every worker process re-runs your entire entry script, so top-level task-generation code (scanning folders, querying a database, ...) runs once per worker. Wrapped in a function it is called **only once, in the master process**, and workers never call it. 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.
222
+ - **`processTask(task, helper)`** — required; return a result or a Promise with result. Results are stored as JSON in `results/succ/<subid>.json`; if a result is not JSON-serializable, the task still counts as succeeded (a placeholder is stored) and a warning shows up in the command line and in monitor tools.
223
+ - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
224
+ - **`taskId`** — task folder name under `taskRootFolder`.
225
+ - **`numberOfWorkers`** how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
226
+ - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
227
+ - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
228
+ - **`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.
229
+ - **`progressBar`** `true` by default: shows a live progress bar in the terminal; set `false` to fall back to plain log output.
230
+ - **`worker_status_poll_ms`** — worker status poll interval in ms; default `2000`; `0` (or `false`) disables the worker status table; `true` means the default rate.
231
+ - **`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.
232
+ - **`shouldTerminate(info)`** — return `true` to terminate the whole process. If it throws, the error is logged and the task is dispatched anyway (treated as "don't terminate").
233
+ - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
234
+ - **`onFinish(report, helper)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, succButSaveFailed, workers }` — `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `succButSaveFailed` counts tasks that ran fine but whose results could not be JSON-serialized (details in `<taskFolder>/.sys/user_messages/`); `workers` is `{ count, crashed }` — the configured worker count and how many workers crashed and were replaced. `helper` walks every persisted task result:
235
+ - **`helper.foreachSuccResult((count, task, result) => {})`** — iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing; a `{type: 'result_serialize_error', message}` placeholder when the value could not be JSON-serialized).
236
+ - **`helper.foreachErrorResult((count, task, err) => {})`** — iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`). A non-`Error` rejection value that is not JSON-serializable is persisted as `{ex: {type: 'unserializable_error_value', ...}}`.
237
+ - **`helper.foreachResult((count, task, result, succeeded) => {})`** — iterate all results regardless of outcome (succeeded first, then failed; `count` runs continuously across both); `succeeded` is `true` for results from `results/succ` and `false` for those from `results/errors` (where `result` is the persisted error object).
238
+ - Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns — don't `await` inside it.
239
+
240
+ **Other options:**
241
+
242
+ - **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`/`restart`.
243
+
244
+ **Task helper** (the `helper` object passed as the second argument of `processTask`):
245
+
246
+ - **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
247
+ - **`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.
248
+ - **`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.
249
+ - **`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`.
250
+
251
+ ### Changelog:
252
+
253
+ - 3.4.1 Rewrite README: simpler structure, all examples verified by test cases
254
+ - 3.4.0 Show per-worker load (ELU/CPU/memory) under progressBar; stability fixes
255
+ - 3.3.0 Unserializable results still succeed; shouldTerminate throwing is safe; progressBar counters added
256
+ - 3.2.3 Support workerPriority: set OS scheduling priority for worker processes
257
+ - 3.2.2 initialTasks accepts a function (sync or async) that is called only once, in the master process
258
+ - 3.2.1 Fix retry bug: user-defined taskCount/index no longer overwritten on task retries
259
+ - 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
260
+ - 3.1.9 Print the multi-tasks-monitor command on startup
261
+ - 3.1.8 Small refinements
262
+ - 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
263
+ - 3.1.6 Fix silent task loss under heavy load with maxTaskRetries
264
+ - 3.1.4 Rewrite methods multiTasks.resume/retry_fails/restart
265
+ - 3.1.0 Support worker broadcast and system events; default numberOfWorkers is now "50%" of CPU cores
266
+ - 3.0.3 Support 'maxTaskRetries'
267
+ - 3.0.2 Support 'taskTimeout'
268
+ - 3.0.1 Versions 3.0.0 and above are maintained by AI
269
+ - 3.0.0 and earlier: see the git history
270
+
271
+ ### Gitee:
272
+
273
+ [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
274
+
275
+ ### License:
276
+
277
+ [MIT](https://opensource.org/license/MIT)