multi-tasks 3.4.0 → 3.5.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
@@ -1,261 +1,245 @@
1
1
  # multi-tasks
2
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.
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
+ **New in 3.5.0 — `task_storage: 'memory'`**: keep all task state in the master process's memory instead of on disk. Same API, one config field — and ~17× faster on huge batches of small tasks. The trade-off: no resume. See [Task storage modes](#task-storage-modes).
6
+
7
+ ![task_storage benchmark: file mode 195.1s vs memory mode 11.2s — 17.4x faster](bench-storage.svg)
4
8
 
5
9
  ### Install:
6
10
 
7
- ```javascript
11
+ ```text
8
12
  npm install multi-tasks
9
13
  ```
10
14
 
11
- Tip: monitor running tasks in real time — see [Real-Time Monitoring](#real-time-monitoring).
12
-
13
- ### How to use:
15
+ ### Quick start:
14
16
 
15
17
  ```javascript
16
- //see examples/example0
17
18
  let multiTasks = require('multi-tasks').multiTasks;
18
19
 
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';
20
+ //your tasks, as a plain array
21
+ let tasks = [];
22
+ for(let i = 0; i < 50; i++){
23
+ tasks.push({name: `task-${i}`});
40
24
  };
41
25
 
42
- //Step3, run!
43
26
  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
27
+ initialTasks: tasks,
28
+ //process one task and return its result (a plain value or a Promise)
29
+ processTask: (task, helper) => {
30
+ return `done: ${task.name}`;
31
+ },
32
+ taskRootFolder: './my-tasks', //progress and results live in ./my-tasks/demo
33
+ taskId: 'demo',
34
+ numberOfWorkers: 3, //worker processes running in parallel
35
+ autoCloseAfterCompletion: true, //exit the process when all tasks are done
36
+ onFinish: (report) => {
37
+ console.log(`${report.succeeded} succeeded, ${report.failed} failed`);
55
38
  },
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
39
  });
66
-
67
40
  ```
68
41
 
69
- ### More about the task processing function
42
+ 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`.
70
43
 
71
- ```javascript
72
- //Examples 1-3 below are independent variants of processTask, use one at a time.
44
+ ### Results and errors:
73
45
 
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;
46
+ `processTask` may return a plain value or a Promise:
79
47
 
80
- return new Promise((resolve, reject)=>{
81
- resolve({
82
- data:`task${taskCount} complete`
83
- })
84
- })
48
+ ```javascript
49
+ let processTask = (task, helper) => {
50
+ let {taskCount} = task; //0-based index of this task
51
+
52
+ return new Promise((resolve, reject) => {
53
+ setTimeout(() => {
54
+ resolve({data: `task${taskCount} complete`});
55
+ }, 100);
56
+ });
85
57
  };
58
+ ```
86
59
 
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
- };
60
+ A failing task never stops the batch — its error is captured and saved to `results/errors`, the rest keep going:
105
61
 
106
- //Example3, generate/throw exceptions in a task method
107
- let processTask = (task, helper)=>{
62
+ ```javascript
63
+ let processTask = (task, helper) => {
108
64
  let {taskCount} = task;
109
65
 
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
66
+ if(taskCount === 3) throw 'exception'; //a thrown value is persisted as-is
67
+ if(taskCount === 4) return Promise.reject({err: 'a test error'}); //a rejection value is persisted as-is
68
+ if(taskCount === 5) aaa = bbb; //a ReferenceError is captured too
114
69
 
115
- return {data:'succ'};
116
- }
70
+ return {data: 'succ'};
71
+ };
72
+ ```
117
73
 
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
- });
74
+ ### Creating new tasks dynamically:
75
+
76
+ ```javascript
77
+ //scenario: crawl a paginated API — each page tells you if there is a next one
78
+ let processTask = async (task, helper) => {
79
+ let page = await fetchPage(task.url); //your logic here
80
+
81
+ if(page.nextUrl){
82
+ //a discovered next page becomes a new task;
83
+ //the pages are finite, so the queue always drains on its own
84
+ helper.createNewTasks({url: page.nextUrl});
85
+ };
86
+
87
+ return {url: task.url, items: page.items};
88
+ };
128
89
 
129
- //Example5, auto-retry a failed task with maxTaskRetries
130
90
  multiTasks({
131
- initialTasks: alltasks,
91
+ initialTasks: [{url: 'https://api.example.com/items?page=1'}], //start from the first page
132
92
  processTask,
133
- taskRootFolder: `../examples-tmp-data/example-retry`,
134
- taskId: 'my-task',
93
+ taskRootFolder: './my-tasks',
94
+ taskId: 'api-crawl',
135
95
  numberOfWorkers: 3,
136
- maxTaskRetries: 2,
96
+ //keep autoCloseAfterCompletion false (the default) while tasks may still be created
137
97
  });
138
-
139
98
  ```
140
99
 
141
- ### Event broadcast
100
+ 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.
142
101
 
143
- ```javascript
102
+ ### Timeouts and retries:
144
103
 
145
- //Example6, broadcast events between workers with helper.emit/helper.setListener
104
+ ```javascript
146
105
  multiTasks({
147
- initialTasks: alltasks,
148
- taskRootFolder: `../examples-tmp-data/example-broadcast`,
149
- taskId: 'my-task',
106
+ initialTasks: tasks,
107
+ processTask,
108
+ taskRootFolder: './my-tasks',
109
+ taskId: 'demo',
150
110
  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
- },
111
+ taskTimeout: 30000, //a task that overruns 30s fails with a timeout error
112
+ maxTaskRetries: 2, //auto-retry a failed task (worker crash, processTask error, or timeout) up to 2 times
158
113
  });
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
114
  ```
180
115
 
181
- ### Performance: runs only once, in the master process
116
+ ### Resuming an interrupted run:
182
117
 
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:
118
+ Just re-run the same code`multiTasks(config)` detects the existing task folder and auto-resumes, re-running only the stuck tasks:
184
119
 
185
120
  ```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
-
121
+ multiTasks(config); //same command, same config as the interrupted run
198
122
  ```
199
123
 
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.
124
+ Or point `initialTasks` at the task foldersame effect:
201
125
 
202
- ### Resuming
126
+ ```javascript
127
+ multiTasks({...config, initialTasks: '/myworks/my_scan_tasks/my-task'});
128
+ ```
203
129
 
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:
130
+ Or resume from the task folder alone the config (including `processTask`) is restored from `task_config.json`:
205
131
 
206
132
  ```javascript
133
+ multiTasks.resume({taskFolder: '/myworks/my_scan_tasks/my-task'}); //taskFolder: the full path of the existing task folder
134
+ ```
207
135
 
208
- //same command, same config as the interrupted run that's all
209
- multiTasks(config);
136
+ **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:
210
137
 
138
+ ```javascript
139
+ multiTasks.resume({taskFolder: '/myworks/my_scan_tasks/my-task', processTask});
211
140
  ```
212
141
 
213
- Or point `initialTasks` at the task foldersame effect:
142
+ Note: a folder created with `task_storage: 'memory'` holds no recoverable stateresuming it throws.
214
143
 
215
- ```javascript
144
+ ### Retrying failed tasks:
216
145
 
217
- multiTasks({ ...config, initialTasks: `/myworks/my_scan_tasks/my-task` });
146
+ Resume an interrupted run and also re-run the tasks that failed (in `progress/finished_with_errors`):
218
147
 
148
+ ```javascript
149
+ multiTasks.retry_fails({taskFolder: '/myworks/my_scan_tasks/my-task'});
219
150
  ```
220
151
 
221
- Or resume from the task folder alone — the config (including `processTask`) is restored from `task_config.json`:
152
+ ### Restarting from scratch:
153
+
154
+ 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`):
222
155
 
223
156
  ```javascript
157
+ multiTasks.restart({taskFolder: '/myworks/my_scan_tasks/my-task'});
158
+ ```
224
159
 
225
- multiTasks.resume({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
160
+ 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.
226
161
 
227
- ```
162
+ ### Task storage modes:
228
163
 
229
- **Note:** revived functions must be **self-contained** (no outer-scope variables; `require(...)` inside the body) otherwise pass them again, they always win:
164
+ By default every task state change is written to disk immediately (`file` mode), which is what makes resume/retry/restart possible. For very large or very fast batches you can trade durability for speed:
230
165
 
231
166
  ```javascript
232
- multiTasks.resume({
233
- taskFolder: `/myworks/my_scan_tasks/my-task`,
167
+ multiTasks({
168
+ initialTasks: tasks,
234
169
  processTask,
170
+ taskRootFolder: './my-tasks',
171
+ taskId: 'demo',
172
+ task_storage: 'memory', //'file' (default) | 'memory'
235
173
  });
236
174
  ```
237
175
 
238
- ### Retrying failed tasks
176
+ - **`file`** (default) — every state change hits disk immediately; resume anytime.
177
+ - **`memory`** — task states, results, logs and user messages live in the master process's memory only; no per-task files are ever written. The task folder is still created with the usual subfolders (`progress`/`results`/`logs` stay empty), holding only `task_config.json` and `.sys/run.log`. Dramatically faster for large batches of small tasks — about 20x faster in benchmarks. Results are still available through `onFinish(report, helper)`. `resume`/`retry_fails`/`restart` are not supported for this mode and throw when called — a killed run is simply gone.
239
178
 
240
- To resume an interrupted run and also retry the tasks that failed (in task folder `progress/finished_with_errors`):
179
+ Benchmark 30,000 tiny tasks × 28 workers, average of 3 runs on the same machine (17–27× across repeated benchmark sessions):
241
180
 
242
- ```javascript
181
+ ![task_storage benchmark: file mode 195.1s vs memory mode 11.2s — 17.4x faster](bench-storage.svg)
243
182
 
244
- multiTasks.retry_fails({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
183
+ **Getting results out in `memory` mode**: since nothing per-task hits disk, `onFinish(report, helper)` is the one and only exit for your data — iterate everything with `helper.foreachResult` (or `foreachSuccResult`/`foreachErrorResult`) and persist it yourself in one batch if you need it kept:
245
184
 
185
+ ```javascript
186
+ onFinish: (report, helper) => {
187
+ helper.foreachResult((count, task, result, succeeded) => {
188
+ //collect / batch-insert / write out — all results are here in memory
189
+ });
190
+ },
246
191
  ```
247
192
 
248
- ### Restarting from scratch
193
+ If you need per-task files anyway, write them in bulk from `onFinish` — do **not** `writeFileSync` inside `processTask`: in benchmarks (30,000 tasks × 28 workers) per-task self-written files made the run ~3.3× slower than pure `memory` (27.7s vs 8.4s), because disk writes still dominate even without the library's bookkeeping. (That said, even the slow do-it-yourself variant beats `file` mode's ~195s — its cost is the resumable state machine, not the writes alone.)
249
194
 
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`):
195
+ Caveats of `memory` mode:
251
196
 
252
- ```javascript
197
+ - **Monitoring**: [multi-tasks-monitor](#real-time-monitoring) reads the task folder, so it has nothing to show for a `memory` run (no task states/results on disk).
198
+ - **Worker crashes**: without `maxTaskRetries`, a crashed worker's in-flight task is not retried — with `file` it stays in `running` until a later `resume` picks it up; with `memory` it is lost for good.
199
+ - **Dynamic tasks**: with `memory`, `helper.createNewTasks` reaches the master over IPC; if the master is killed before applying them, tasks created but not yet applied are lost. (`file` mode writes them to disk from the worker directly.)
200
+ - **Dynamic tasks with `autoCloseAfterCompletion`**: in `memory` mode the master pops straight from the in-memory queue — there is no on-disk re-read fallback like in `file` mode, and no settle delay before killing an idle worker. A task another worker just sent via `helper.createNewTasks` may still be in IPC transit when the master decides the queue is empty, so it can miss the dispatch window and the workers get closed. If your workers create tasks dynamically, keep `autoCloseAfterCompletion` at its default `false`.
253
201
 
254
- multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes all previous progress and results, then re-runs all the initial tasks
202
+ ### Broadcasting events between workers:
255
203
 
204
+ ```javascript
205
+ multiTasks({
206
+ initialTasks: tasks,
207
+ processTask: (task, helper) => {
208
+ helper.setListener('task-done', (payload, meta) => {
209
+ console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
210
+ });
211
+ helper.emit('task-done', {seq: task.seq}); //broadcast to all workers
212
+ return {data: 'succ'};
213
+ },
214
+ taskRootFolder: './my-tasks',
215
+ taskId: 'demo',
216
+ numberOfWorkers: 3,
217
+ });
256
218
  ```
257
219
 
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.
220
+ ### System events:
221
+
222
+ Send events from a worker to the master with `helper.emitSys`:
223
+
224
+ ```javascript
225
+ multiTasks({
226
+ initialTasks: tasks,
227
+ setSysListener: (event, payload, meta) => {
228
+ console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
229
+ },
230
+ processTask: (task, helper) => {
231
+ helper.emitSys('task-started', {seq: task.seq});
232
+ if(task.fatal){
233
+ helper.emitSys('TERMINATE_ALL_WORKERS'); //built-in event: stop everything
234
+ return;
235
+ };
236
+ return {data: 'succ'};
237
+ },
238
+ taskRootFolder: './my-tasks',
239
+ taskId: 'demo',
240
+ numberOfWorkers: 3,
241
+ });
242
+ ```
259
243
 
260
244
  ### Real-Time Monitoring
261
245
 
@@ -264,27 +248,24 @@ Monitor your running tasks with [multi-tasks-monitor](https://www.npmjs.com/pack
264
248
  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
249
 
266
250
  ```text
267
- monitor install cmd : npm install multi-tasks-monitor
268
- monitor start cmd : npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
251
+ npm install multi-tasks-monitor
252
+ npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
269
253
  ```
270
254
 
271
- For command-line options and more, see the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
272
-
273
255
  ### API:
274
256
 
275
- The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
276
-
277
257
  **Entry functions:**
278
258
 
279
259
  - **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
280
- - **`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.
260
+ - **`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.
281
261
  - **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
282
- - **`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.**
262
+ - **`multiTasks.restart(config)`** — wipe the task folder's progress and results and re-run from scratch (`{taskFolder}`). **All previous progress and results are deleted.**
283
263
 
284
264
  **Config options:**
285
265
 
286
- - **`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.
287
- - **`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.
266
+ - **`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.
267
+ - 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.
268
+ - **`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.
288
269
  - **`taskRootFolder`** — the task run folder (named by `taskId`) is created under this directory.
289
270
  - **`taskId`** — task folder name under `taskRootFolder`.
290
271
  - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
@@ -293,14 +274,15 @@ The API has three parts: the entry functions, the config options, and the `helpe
293
274
  - **`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.
294
275
  - **`progressBar`** — `true` by default: shows a live progress bar in the terminal; set `false` to fall back to plain log output.
295
276
  - **`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.
277
+ - **`task_storage`** — `'file'` (default) | `'memory'`: where task states and results are kept; see "Task storage modes".
296
278
  - **`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.
297
279
  - **`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").
298
280
  - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
299
- - **`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:
300
- - **`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).
281
+ - **`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:
282
+ - **`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).
301
283
  - **`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', ...}}`.
302
284
  - **`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).
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.
285
+ - 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. In `memory` mode the helpers iterate the in-memory results the same way (in completion order).
304
286
 
305
287
  **Other options:**
306
288
 
@@ -315,6 +297,8 @@ The API has three parts: the entry functions, the config options, and the `helpe
315
297
 
316
298
  ### Changelog:
317
299
 
300
+ - 3.5.0 Add `task_storage: 'memory'` mode (~17x faster, no resume)
301
+ - 3.4.1 Rewrite README: simpler structure, all examples verified by test cases
318
302
  - 3.4.0 Show per-worker load (ELU/CPU/memory) under progressBar; stability fixes
319
303
  - 3.3.0 Unserializable results still succeed; shouldTerminate throwing is safe; progressBar counters added
320
304
  - 3.2.3 Support workerPriority: set OS scheduling priority for worker processes
@@ -325,47 +309,12 @@ The API has three parts: the entry functions, the config options, and the `helpe
325
309
  - 3.1.8 Small refinements
326
310
  - 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
327
311
  - 3.1.6 Fix silent task loss under heavy load with maxTaskRetries
328
- - 3.1.5 Update README
329
312
  - 3.1.4 Rewrite methods multiTasks.resume/retry_fails/restart
330
- - 3.1.3 Update README
331
- - 3.1.2 Update README
332
- - 3.1.1 Fix readme documentation
333
313
  - 3.1.0 Support worker broadcast and system events; default numberOfWorkers is now "50%" of CPU cores
334
- - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
335
314
  - 3.0.3 Support 'maxTaskRetries'
336
315
  - 3.0.2 Support 'taskTimeout'
337
316
  - 3.0.1 Versions 3.0.0 and above are maintained by AI
338
- - 2.2.0 Support Resuming from an interrupted task
339
- - 2.1.2 Update readme
340
- - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
341
- - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
342
- - 2.0.9 Update readme examples
343
- - 2.0.8 Update readme examples
344
- - 2.0.7 Update changelog
345
- - 2.0.6 Update readme, remove failed examples
346
- - 2.0.5 Update readme examples
347
- - 2.0.4 Support resume from a failed task
348
- - 2.0.3 Fix: mkdir bug on windows
349
- - 2.0.2 new feature: support shouldTerminate
350
- - 2.0.1 Avoid possible I/O conflicts.
351
- - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
352
- - 1.2.8 Fix: create task folder failed on MacOS
353
- - 1.2.7 Small updates
354
- - 1.2.6 Support onFinish event
355
- - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
356
- - 1.2.4 Fix: opt.numberOfWorkers not work
357
- - 1.2.3 Update README
358
- - 1.2.2 Update README and examples
359
- - 1.2.1 Handle exceptions and errors in subtasks
360
- - 1.2.0 Simplified usage by providing the function way and support return Promise
361
- - 1.1.4 Remove make-dir
362
- - 1.1.3 Simplified usage, see example0
363
- - 1.1.2
364
- - 1.1.1 Rename files, updated changelog
365
- - 1.1.0 Simplified the usage of a customized Consumer, see example0
366
- - 1.0.8 Fix examples
367
- - 1.0.7 Remove moment
368
- - 1.0.6 Performance optimization
317
+ - 3.0.0 and earlier: see the git history
369
318
 
370
319
  ### Gitee:
371
320
 
@@ -373,4 +322,4 @@ The API has three parts: the entry functions, the config options, and the `helpe
373
322
 
374
323
  ### License:
375
324
 
376
- [MIT](https://opensource.org/license/MIT)
325
+ [MIT](https://opensource.org/license/MIT)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.4.0",
3
+ "version": "3.5.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": [
@@ -1,4 +1,5 @@
1
1
  function v1tov2(config) {
2
+ if(!config) throw new Error('config is required');//缺参时给有语义的报错, 而不是裸 TypeError(兼容入口 multiTasks/resume/retry_fails/restart 都汇到这里)
2
3
  if(config.multi_task_parent_folder && !config.taskRootFolder) config.taskRootFolder = config.multi_task_parent_folder;//backward compatible
3
4
  delete config.multi_task_parent_folder;
4
5
 
package/utils/randoms.js CHANGED
@@ -2,10 +2,10 @@
2
2
  const crypto = require('crypto');
3
3
  const fmtDigit = (n)=>{
4
4
  return n > 9 ? "" + n: "0" + n;
5
- }
5
+ };
6
6
  const getDateTimeTxt = ()=>{
7
7
  let d=new Date();
8
- let YYYY=d.getFullYear()
8
+ let YYYY=d.getFullYear();
9
9
  let MM=fmtDigit(d.getMonth()+1);
10
10
  let DD=fmtDigit(d.getDate());
11
11
  let HH=fmtDigit(d.getHours());
@@ -6,7 +6,7 @@ const PERCENT_PATTERN = /^(\d+(?:\.\d+)?)%$/;
6
6
 
7
7
  // 解析 numberOfWorkers 配置:
8
8
  // - undefined -> 默认 "50%", 按核数四舍五入折算, 最小为 1;
9
- // - number -> 原样返回(现状行为不变);
9
+ // - number -> 必须是 >=1 的整数(0 会静默起不来 worker、小数/负数行为隐晦, fail-fast);
10
10
  // - string -> 必须是 "数字%" 形式且百分比在 1-100 之间, 按核数四舍五入折算, 最小为 1;
11
11
  // - 其余 -> 抛 Error。
12
12
  const percentToWorkers = (percent) => {
@@ -18,7 +18,9 @@ const resolveNumberOfWorkers = (value) => {
18
18
  return percentToWorkers(50);
19
19
  };
20
20
  if (typeof value === 'number') {
21
- return value;
21
+ if (Number.isInteger(value) && value >= 1) {
22
+ return value;
23
+ };
22
24
  };
23
25
  if (typeof value === 'string') {
24
26
  const match = value.trim().match(PERCENT_PATTERN);