multi-tasks 3.1.2 → 3.1.4

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,257 +1,292 @@
1
- # multi-tasks
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`.
6
-
7
- ### Install:
8
-
9
- ```javascript
10
- npm install multi-tasks
11
- ```
12
-
13
- ### API:
14
-
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`.
45
-
46
- ### How to use:
47
-
48
- ```javascript
49
- //see examples/example0
50
- let multiTasks = require('multi-tasks').multiTasks;
51
-
52
- //Step1, create the tasks to run in parallel as an array.
53
- let alltasks = [];
54
- for(let i=0;i<50;i++){
55
- alltasks.push({
56
- name: `task-${i}`,
57
- data: `This prop is for a subtask`
58
- });
59
- };
60
-
61
- //Step2, provide a function that processes each sub-task and returns the result
62
- let processTask = (task, helper)=>{
63
- let {taskCount} = task;//get your task data
64
-
65
- //Run your processing logic here...
66
-
67
- //then return the result as a plain javascript object or an async promise
68
- return 'result data';
69
- };
70
-
71
- //Step3, run!
72
- multiTasks({
73
- initialTasks: alltasks,
74
- processTask,
75
- taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and result files; you can check the progress here
76
- taskId: 'my-task',
77
- numberOfWorkers: 3, //how many workers are working in parallel
78
- //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
79
- //maxTaskRetries: 2, //optional, auto-retry a failed task (worker crash, processTask error, or timeout); retried tasks go back to the queue
80
- //autoCloseAfterCompletion: true, //if you have dynamically generated new tasks, set this to false
81
- shouldTerminate:(info)=>{
82
- //return true if you need to terminate the whole process
83
- },
84
- onFinish: (report)=>{
85
- console.log('finish callback', report);
86
- }
87
- });
88
-
89
- ```
90
-
91
- ### More about the task processing function
92
-
93
- ```javascript
94
- //Example1, return a promise for async processes,
95
- // you can return a promise or a non-promise result,
96
- // all result data can be found in the results/succ folder
97
- let processTask = (task, helper)=>{
98
- let {taskCount} = task;
99
-
100
- return new Promise((resolve, reject)=>{
101
- resolve({
102
- data:`task${taskCount} complete`
103
- })
104
- })
105
- };
106
-
107
- //Example2, dynamically create a new task while processing
108
- let processTask = (task, helper)=>{
109
- let {taskCount} = task;
110
-
111
- if(taskCount % 2 === 0){
112
- //create a new task if needed
113
- helper.createNewTasks({
114
- msg:'a new task'
115
- });
116
- return;
117
- }
118
-
119
- return new Promise((resolve, reject)=>{
120
- resolve({
121
- data:`task${taskCount} complete`
122
- })
123
- })
124
- };
125
-
126
- //Example3, generate/throw exceptions in a task method
127
- let processTask = (task, helper)=>{
128
- let {taskCount} = task;
129
-
130
- //Demo of exceptions/errors: they are captured and saved in the results/errors folder
131
- if(taskCount===3) throw 'exception';
132
- if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
133
- if(taskCount===5) aaa = bbb;//this ReferenceError will be captured by multi-tasks
134
-
135
- return {data:'succ'};
136
- }
137
-
138
- //Example4, timeout: a task that does not finish within taskTimeout is
139
- // treated as failed with a timeout error
140
- multiTasks({
141
- initialTasks: alltasks,
142
- processTask,
143
- taskRootFolder: `../examples-tmp-data/example-timeout`,
144
- taskId: 'my-task',
145
- numberOfWorkers: 3,
146
- taskTimeout: 30000,
147
- });
148
-
149
- //Example5, auto-retry a failed task with maxTaskRetries
150
- multiTasks({
151
- initialTasks: alltasks,
152
- processTask,
153
- taskRootFolder: `../examples-tmp-data/example-retry`,
154
- taskId: 'my-task',
155
- numberOfWorkers: 3,
156
- maxTaskRetries: 2,
157
- });
158
-
159
- //Example6, broadcast events between workers with helper.emit/helper.setListener
160
- multiTasks({
161
- initialTasks: alltasks,
162
- taskRootFolder: `../examples-tmp-data/example-broadcast`,
163
- taskId: 'my-task',
164
- numberOfWorkers: 3,
165
- processTask: (task, helper) => {
166
- helper.setListener('task-done', (payload, meta) => {
167
- console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
168
- });
169
- helper.emit('task-done', {seq: task.seq});
170
- return {data:'succ'};
171
- },
172
- });
173
-
174
- //Example7, system events from a worker to the master with helper.emitSys
175
- multiTasks({
176
- initialTasks: alltasks,
177
- taskRootFolder: `../examples-tmp-data/example-sysevent`,
178
- taskId: 'my-task',
179
- numberOfWorkers: 3,
180
- setSysListener: (event, payload, meta) => {
181
- console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
182
- },
183
- processTask: (task, helper) => {
184
- helper.emitSys('task-started', {seq: task.seq});
185
- if(task.fatal){
186
- helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
187
- return;
188
- };
189
- return {data:'succ'};
190
- },
191
- });
192
-
193
- ```
194
-
195
- ### Resuming
196
-
197
- If the execution is interrupted (e.g. a power outage), resume it like this:
198
-
199
- ```javascript
200
-
201
- multiTasks({
202
- initialTasks: `/myworks/my_scan_tasks/`, //point 'initialTasks' to the interrupted task directory; multi-tasks reads the tasks in its 'new' folder and continues execution
203
- ...
204
- ...//Other configurations remain unchanged
205
- ...
206
- });
207
-
208
- ```
209
-
210
- ### Changelog:
211
-
212
- - 3.1.2 Update README
213
- - 3.1.1 Fix readme documentation
214
- - 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)
215
- - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
216
- - 3.0.3 Support 'maxTaskRetries'
217
- - 3.0.2 Support 'taskTimeout'
218
- - 3.0.1 Versions 3.0.0 and above are maintained by AI
219
- - 2.2.0 Support Resuming from an interrupted task
220
- - 2.1.2 Update readme
221
- - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
222
- - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
223
- - 2.0.9 Update readme examples
224
- - 2.0.8 Update readme examples
225
- - 2.0.7 Update changelog
226
- - 2.0.6 Update readme, remove failed examples
227
- - 2.0.5 Update readme examples
228
- - 2.0.4 Support resume from a failed task
229
- - 2.0.3 Fix: mkdir bug on windows
230
- - 2.0.2 new feature: support shouldTerminate
231
- - 2.0.1 Avoid possible I/O conflicts.
232
- - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
233
- - 1.2.8 Fix: create task folder failed on MacOS
234
- - 1.2.7 Small updates
235
- - 1.2.6 Support onFinish event
236
- - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
237
- - 1.2.4 Fix: opt.numberOfWorkers not work
238
- - 1.2.3 Update README
239
- - 1.2.2 Update README and examples
240
- - 1.2.1 Handle exceptions and errors in subtasks
241
- - 1.2.0 Simplified usage by providing the function way and support return Promise
242
- - 1.1.4 Remove make-dir
243
- - 1.1.3 Simplified usage, see example0
244
- - 1.1.2
245
- - 1.1.1 Rename files, updated changelog
246
- - 1.1.0 Simplified the usage of a customized Consumer, see example0
247
- - 1.0.8 Fix examples
248
- - 1.0.7 Remove moment
249
- - 1.0.6 Performance optimization
250
-
251
- ### Gitee:
252
-
253
- [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
254
-
255
- ### License:
256
-
257
- [MIT](https://opensource.org/license/MIT)
1
+ # multi-tasks
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`.
6
+
7
+ ### Install:
8
+
9
+ ```javascript
10
+ npm install multi-tasks
11
+ ```
12
+
13
+ ### API:
14
+
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.resume(config)`** — resume an interrupted run; 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.
21
+ - **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
22
+ - **`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.**
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`** — the task run folder (named by `taskId`) is created under this directory.
29
+ - **`taskId`** — task folder name under `taskRootFolder`.
30
+ - **`numberOfWorkers`** — how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
31
+ - **`taskTimeout`** — optional, milliseconds; an overdue task fails with a timeout error.
32
+ - **`maxTaskRetries`** — optional, max times a failed task is auto-retried.
33
+ - **`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.
34
+ - **`shouldTerminate(info)`** — return `true` to terminate the whole process.
35
+ - **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
36
+ - **`onFinish(report)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`); `report` is an empty object for now.
37
+
38
+ **Other options:**
39
+
40
+ - **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`.
41
+
42
+ **Task helper** (the `helper` object passed as the second argument of `processTask`):
43
+
44
+ - **`helper.createNewTasks(tasks)`** — create new tasks dynamically while processing.
45
+ - **`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.
46
+ - **`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.
47
+ - **`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`.
48
+
49
+ ### How to use:
50
+
51
+ ```javascript
52
+ //see examples/example0
53
+ let multiTasks = require('multi-tasks').multiTasks;
54
+
55
+ //Step1, create the tasks to run in parallel as an array.
56
+ let alltasks = [];
57
+ for(let i=0;i<50;i++){
58
+ alltasks.push({
59
+ name: `task-${i}`,
60
+ data: `This prop is for a subtask`
61
+ });
62
+ };
63
+
64
+ //Step2, provide a function that processes each sub-task and returns the result
65
+ let processTask = (task, helper)=>{
66
+ let {taskCount} = task;//get your task data
67
+
68
+ //Run your processing logic here...
69
+
70
+ //then return the result as a plain javascript object or an async promise
71
+ return 'result data';
72
+ };
73
+
74
+ //Step3, run!
75
+ multiTasks({
76
+ initialTasks: alltasks,
77
+ processTask,
78
+ taskRootFolder: `../examples-tmp-data/example0`, //a directory to store progress and result files; you can check the progress here
79
+ taskId: 'my-task',
80
+ numberOfWorkers: 3, //how many workers are working in parallel
81
+ //taskTimeout: 30000, //optional, in milliseconds, an overdue task is treated as failed with a timeout error
82
+ //maxTaskRetries: 2, //optional, auto-retry a failed task (worker crash, processTask error, or timeout); retried tasks go back to the queue
83
+ //autoCloseAfterCompletion: true, //set true to exit the process when all tasks are done; keep it false (default) if you create new tasks dynamically
84
+ shouldTerminate:(info)=>{
85
+ //return true if you need to terminate the whole process
86
+ },
87
+ onFinish: (report)=>{
88
+ console.log('finish callback', report);
89
+ }
90
+ });
91
+
92
+ ```
93
+
94
+ ### More about the task processing function
95
+
96
+ ```javascript
97
+ //Examples 1-3 below are independent variants of processTask, use one at a time.
98
+
99
+ //Example1, return a promise for async processes,
100
+ // you can return a promise or a non-promise result,
101
+ // all result data can be found in the results/succ folder
102
+ let processTask = (task, helper)=>{
103
+ let {taskCount} = task;
104
+
105
+ return new Promise((resolve, reject)=>{
106
+ resolve({
107
+ data:`task${taskCount} complete`
108
+ })
109
+ })
110
+ };
111
+
112
+ //Example2, dynamically create a new task while processing
113
+ let processTask = (task, helper)=>{
114
+ let {taskCount} = task;
115
+
116
+ if(taskCount % 2 === 0){
117
+ //create a new task if needed
118
+ helper.createNewTasks({
119
+ msg:'a new task'
120
+ });
121
+ return;
122
+ }
123
+
124
+ return new Promise((resolve, reject)=>{
125
+ resolve({
126
+ data:`task${taskCount} complete`
127
+ })
128
+ })
129
+ };
130
+
131
+ //Example3, generate/throw exceptions in a task method
132
+ let processTask = (task, helper)=>{
133
+ let {taskCount} = task;
134
+
135
+ //Demo of exceptions/errors: they are captured and saved in the results/errors folder
136
+ if(taskCount===3) throw 'exception';
137
+ if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
138
+ if(taskCount===5) aaa = bbb;//this ReferenceError will be captured by multi-tasks
139
+
140
+ return {data:'succ'};
141
+ }
142
+
143
+ //Example4, timeout: a task that does not finish within taskTimeout is
144
+ // treated as failed with a timeout error
145
+ multiTasks({
146
+ initialTasks: alltasks,
147
+ processTask,
148
+ taskRootFolder: `../examples-tmp-data/example-timeout`,
149
+ taskId: 'my-task',
150
+ numberOfWorkers: 3,
151
+ taskTimeout: 30000,
152
+ });
153
+
154
+ //Example5, auto-retry a failed task with maxTaskRetries
155
+ multiTasks({
156
+ initialTasks: alltasks,
157
+ processTask,
158
+ taskRootFolder: `../examples-tmp-data/example-retry`,
159
+ taskId: 'my-task',
160
+ numberOfWorkers: 3,
161
+ maxTaskRetries: 2,
162
+ });
163
+
164
+ //Example6, broadcast events between workers with helper.emit/helper.setListener
165
+ multiTasks({
166
+ initialTasks: alltasks,
167
+ taskRootFolder: `../examples-tmp-data/example-broadcast`,
168
+ taskId: 'my-task',
169
+ numberOfWorkers: 3,
170
+ processTask: (task, helper) => {
171
+ helper.setListener('task-done', (payload, meta) => {
172
+ console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
173
+ });
174
+ helper.emit('task-done', {seq: task.seq});
175
+ return {data:'succ'};
176
+ },
177
+ });
178
+
179
+ //Example7, system events from a worker to the master with helper.emitSys
180
+ multiTasks({
181
+ initialTasks: alltasks,
182
+ taskRootFolder: `../examples-tmp-data/example-sysevent`,
183
+ taskId: 'my-task',
184
+ numberOfWorkers: 3,
185
+ setSysListener: (event, payload, meta) => {
186
+ console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
187
+ },
188
+ processTask: (task, helper) => {
189
+ helper.emitSys('task-started', {seq: task.seq});
190
+ if(task.fatal){
191
+ helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
192
+ return;
193
+ };
194
+ return {data:'succ'};
195
+ },
196
+ });
197
+
198
+ ```
199
+
200
+ ### Resuming
201
+
202
+ If the execution is interrupted (e.g. a power outage), resume it by pointing to the task folder nothing else is needed, the config (including `processTask`) is restored from `task_config.json`:
203
+
204
+ ```javascript
205
+
206
+ multiTasks.resume({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
207
+
208
+ ```
209
+
210
+ Re-running `multiTasks(config)` (or pointing `initialTasks` at the folder) auto-resumes too; passed config wins over the snapshot.
211
+
212
+ **Note:** revived functions must be **self-contained** (no outer-scope variables; `require(...)` inside the body) — otherwise pass them again, they always win:
213
+
214
+ ```javascript
215
+ multiTasks.resume({
216
+ taskFolder: `/myworks/my_scan_tasks/my-task`,
217
+ processTask,
218
+ });
219
+ ```
220
+
221
+ ### Retrying failed tasks
222
+
223
+ To resume an interrupted run and also retry the tasks that failed (in task folder `progress/finished_with_errors`):
224
+
225
+ ```javascript
226
+
227
+ multiTasks.retry_fails({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //taskFolder: the full path of the existing task folder
228
+
229
+ ```
230
+
231
+ ### Restarting from scratch
232
+
233
+ 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`):
234
+
235
+ ```javascript
236
+
237
+ multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes all previous progress and results, then re-runs all the initial tasks
238
+
239
+ ```
240
+
241
+ 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. (**Breaking change:** the name `restart` was the old name of `retry_fails` in v2 and in the published v3.0–v3.1.x releases; this is a different API — calling it no longer retries failed tasks, it wipes the folder and re-runs everything.)
242
+
243
+ ### Changelog:
244
+
245
+ - 3.1.4 Rewrite `multiTasks.resume`, `multiTasks.retry_fails` and `multiTasks.restart`
246
+ - 3.1.3 Update README
247
+ - 3.1.2 Update README
248
+ - 3.1.1 Fix readme documentation
249
+ - 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)
250
+ - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
251
+ - 3.0.3 Support 'maxTaskRetries'
252
+ - 3.0.2 Support 'taskTimeout'
253
+ - 3.0.1 Versions 3.0.0 and above are maintained by AI
254
+ - 2.2.0 Support Resuming from an interrupted task
255
+ - 2.1.2 Update readme
256
+ - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
257
+ - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
258
+ - 2.0.9 Update readme examples
259
+ - 2.0.8 Update readme examples
260
+ - 2.0.7 Update changelog
261
+ - 2.0.6 Update readme, remove failed examples
262
+ - 2.0.5 Update readme examples
263
+ - 2.0.4 Support resume from a failed task
264
+ - 2.0.3 Fix: mkdir bug on windows
265
+ - 2.0.2 new feature: support shouldTerminate
266
+ - 2.0.1 Avoid possible I/O conflicts.
267
+ - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
268
+ - 1.2.8 Fix: create task folder failed on MacOS
269
+ - 1.2.7 Small updates
270
+ - 1.2.6 Support onFinish event
271
+ - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
272
+ - 1.2.4 Fix: opt.numberOfWorkers not work
273
+ - 1.2.3 Update README
274
+ - 1.2.2 Update README and examples
275
+ - 1.2.1 Handle exceptions and errors in subtasks
276
+ - 1.2.0 Simplified usage by providing the function way and support return Promise
277
+ - 1.1.4 Remove make-dir
278
+ - 1.1.3 Simplified usage, see example0
279
+ - 1.1.2
280
+ - 1.1.1 Rename files, updated changelog
281
+ - 1.1.0 Simplified the usage of a customized Consumer, see example0
282
+ - 1.0.8 Fix examples
283
+ - 1.0.7 Remove moment
284
+ - 1.0.6 Performance optimization
285
+
286
+ ### Gitee:
287
+
288
+ [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
289
+
290
+ ### License:
291
+
292
+ [MIT](https://opensource.org/license/MIT)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-tasks",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
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": [
@@ -16,7 +16,7 @@
16
16
  "example": "examples"
17
17
  },
18
18
  "engines": {
19
- "node": ">=14"
19
+ "node": ">=14.14"
20
20
  },
21
21
  "scripts": {
22
22
  "test": "jest",
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ // config 里的函数型配置键: 写 task_config.json 快照时序列化为源码字符串,
4
+ // resume 时复活为函数。未来新增函数型配置键在此登记。
5
+ const FUNCTION_CONFIG_KEYS = ['processTask', 'onFinish', 'shouldTerminate', 'setSysListener'];
6
+
7
+ //返回浅副本: 函数型键的函数值替换为源码字符串, 其余值原样保留。
8
+ const serializeConfig = (config) => {
9
+ const out = { ...config };
10
+ FUNCTION_CONFIG_KEYS.forEach((key) => {
11
+ if (typeof out[key] === 'function') {
12
+ out[key] = out[key].toString();
13
+ };
14
+ });
15
+ return out;
16
+ };
17
+
18
+ //返回浅副本: 函数型键的字符串值复活为函数。复活环境注入 require/module/exports,
19
+ //函数体内可直接 require(...)。eval 失败或复活结果非函数时抛 Error(消息带键名)。
20
+ const reviveConfig = (config) => {
21
+ const out = { ...config };
22
+ FUNCTION_CONFIG_KEYS.forEach((key) => {
23
+ if (typeof out[key] === 'string') {
24
+ const src = out[key];
25
+ let revived;
26
+ try {
27
+ revived = new Function('require', 'module', 'exports', `return (${src}\n);`)(require, module, exports);
28
+ } catch (e) {
29
+ //方法简写(如 "processTask(task) {...}" 或 "async processTask(task) {...}")需包成函数声明再复活
30
+ let decl = src.startsWith('async ') ? `async function ${src.slice('async '.length)}` : `function ${src}`;
31
+ try {
32
+ revived = new Function('require', 'module', 'exports', `return (${decl}\n);`)(require, module, exports);
33
+ } catch (e2) {
34
+ throw new Error(`Failed to revive config function "${key}" from task_config.json: ${e.message}`);
35
+ };
36
+ };
37
+ if (typeof revived !== 'function') {
38
+ throw new Error(`Failed to revive config function "${key}" from task_config.json: source is not a function`);
39
+ };
40
+ out[key] = revived;
41
+ };
42
+ });
43
+ return out;
44
+ };
45
+
46
+ module.exports = { FUNCTION_CONFIG_KEYS, serializeConfig, reviveConfig };
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const pathutil = require('path');
3
3
  const makeDir = require('../utils/makedir');
4
4
  const randomsUtil = require('../utils/randoms');
5
+ const { serializeConfig } = require('../utils/serializeFns');
5
6
 
6
7
  let allSubTasks = {
7
8
  new:[],
@@ -20,13 +21,16 @@ const TaskMgr = {
20
21
  initFolders:()=>{
21
22
  let {taskRootFolder, masterTaskId} = main_config;
22
23
  const taskFolder = pathutil.resolve(taskRootFolder, masterTaskId);
23
- if(fs.existsSync(taskFolder) && !(main_config.__resume || main_config.__restart)){
24
+ if(fs.existsSync(taskFolder) && !(main_config.__resume || main_config.__retryFails || main_config.__restart)){
24
25
  let files = fs.readdirSync(taskFolder);
25
26
  if(files.length > 0){
26
27
  console.log('[FATAL] taskRootFolder already exist and not empty, delete it and try again: ', taskFolder);
27
28
  return process.exit(0);
28
29
  }
29
30
  };
31
+ if(main_config.__restart && fs.existsSync(taskFolder)){
32
+ TaskMgr.resetTaskFolders(taskFolder);//restart: 整目录重置, 随后由下方 makeDir 重建
33
+ };
30
34
  const progressRootFolder = pathutil.resolve(taskFolder, './progress');
31
35
  const resultsRootFolder = pathutil.resolve(taskFolder, './results');
32
36
  const logsRootFolder = pathutil.resolve(taskFolder, './logs');
@@ -61,9 +65,12 @@ const TaskMgr = {
61
65
  taskFolderResultFailed,
62
66
  };
63
67
 
64
- //for restart a failed task
65
- let main_config2 = JSON.parse(JSON.stringify(main_config));
68
+ //for resuming an interrupted task
69
+ let main_config2 = JSON.parse(JSON.stringify(serializeConfig(main_config)));//函数型配置键先序列化为源码字符串, 供 resume 从快照复活
66
70
  delete main_config2.tasks;//will load from folder 'new', so no need the early tasks data
71
+ Object.keys(main_config2).forEach((key)=>{
72
+ if(key.startsWith('__')) delete main_config2[key];//__-prefixed keys are one-shot runtime markers, never persisted
73
+ });
67
74
  fs.writeFileSync(pathutil.resolve(taskFolder, 'task_config.json'), JSON.stringify(main_config2));
68
75
  return main_config;
69
76
  },
@@ -250,5 +257,8 @@ const TaskMgr = {
250
257
  delete task.__retryCount;
251
258
  fs.writeFileSync(fpath, JSON.stringify(task));
252
259
  },
260
+ resetTaskFolders:(taskFolder)=>{
261
+ fs.rmSync(taskFolder, {recursive: true, force: true});
262
+ },
253
263
  };
254
264
  module.exports = TaskMgr;
@@ -10,12 +10,17 @@ const resume = (config)=>{
10
10
  let {taskRootFolder, taskId} = config;
11
11
  let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
12
12
 
13
- //load from config file
13
+ //load from config file: 旧快照作底(兼作任务目录合法性校验, 不存在抛 ENOENT),
14
+ //本次调用传入的配置覆盖同名字段——快照只补未传入的字段
14
15
  let task_config = JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json')));
16
+ Object.keys(task_config).forEach((key)=>{
17
+ if(key.startsWith('__')) delete task_config[key];//读侧剔除一次性标记, 兼容 <=3.1.3 旧快照
18
+ });
15
19
  config = {
16
- ...config,
17
- ...task_config
18
- }
20
+ ...task_config,
21
+ ...config
22
+ };
23
+ config.masterTaskId = taskId;
19
24
 
20
25
  TaskMgr.load(config);
21
26
  config = TaskMgr.initFolders(config);
@@ -29,11 +34,11 @@ const resume = (config)=>{
29
34
  let subid = fname.replace(/\.task\.json$/, '');
30
35
  promises.push(TaskMgr.mvTaskPromise(subid, 'running', 'new'));
31
36
  });
32
- if(config.__restart){
37
+ if(config.__retryFails){
33
38
  errfiles.forEach((fname)=>{
34
39
  let subid = fname.replace(/\.task\.json$/, '');
35
40
  promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
36
- TaskMgr.resetTaskRetry(subid);//restart 重跑失败任务时重置重试计数(resume 不重置)
41
+ TaskMgr.resetTaskRetry(subid);//retry_fails 重跑失败任务时重置重试计数(resume 不重置)
37
42
  }));
38
43
  });
39
44
  };
package/workers/index.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const pathUtil = require('path');
3
3
  const Multithread = require('./Multithread');
4
4
  const backwardv1 = require('../utils/backward-v1');
5
+ const { FUNCTION_CONFIG_KEYS, reviveConfig } = require('../utils/serializeFns');
5
6
 
6
7
  const _toAbsPath = (path)=>{
7
8
  if(!path) return path;
@@ -10,6 +11,38 @@ const _toAbsPath = (path)=>{
10
11
  return abspath;
11
12
  };
12
13
 
14
+ const _normalizeResumeArg = (method, config)=>{
15
+ if(typeof config === 'string'){
16
+ throw new Error(`method ${method} need a config object with param "taskFolder" (string path form is no longer supported)`);
17
+ };
18
+ return backwardv1.v1tov2(config);
19
+ };
20
+
21
+ //读取任务目录里的 task_config.json 快照, 复活其中的函数字符串后作底;
22
+ //用户传入的同名字段优先(合并结果会随 resume 被 initFolders 重写回快照, 成为新基线)。
23
+ const _mergeSnapshotConfig = (config)=>{
24
+ let snapshot = JSON.parse(fs.readFileSync(pathUtil.resolve(config.taskFolder, 'task_config.json')));
25
+ Object.keys(snapshot).forEach((key)=>{
26
+ if(key.startsWith('__')) delete snapshot[key];//读侧同样剔除一次性标记: 兼容 <=3.1.3 旧版本写出的带标记快照(防 stale 标记误触发 wipe/错走链路)
27
+ });
28
+ let userConfig = { ...config };
29
+ if(typeof userConfig.initialTasks === 'string'){
30
+ delete userConfig.initialTasks;//目录指针用法(auto-resume), 不应覆盖/污染快照里的任务数组
31
+ };
32
+ //用户已传入同名函数时, 快照里的源码字符串无需复活(反正会被覆盖)——容忍不可复活的源码(如绑定函数)
33
+ FUNCTION_CONFIG_KEYS.forEach((key)=>{
34
+ if(typeof userConfig[key] === 'function' && typeof snapshot[key] === 'string'){
35
+ delete snapshot[key];
36
+ };
37
+ });
38
+ let revived = reviveConfig(snapshot);
39
+ let merged = { ...revived, ...userConfig };
40
+ if(typeof merged.processTask !== 'function'){
41
+ throw new Error('FATAL: no processTask available: task_config.json has none and none was passed in config.');
42
+ };
43
+ return merged;
44
+ };
45
+
13
46
  const start = (config)=>{
14
47
  config = backwardv1.v1tov2(config);
15
48
 
@@ -19,28 +52,51 @@ const start = (config)=>{
19
52
  Multithread.start(config);
20
53
  };
21
54
  const resume = (config)=>{
22
- config = backwardv1.v1tov2(config);
55
+ config = _normalizeResumeArg('resume', config);
23
56
  config.__resume = true;
24
57
 
25
- config.initialTasks = [];
26
-
27
58
  config.taskRootFolder = _toAbsPath(config.taskRootFolder);
28
59
  config.taskFolder = _toAbsPath(config.taskFolder);
60
+ if(!config.taskFolder){
61
+ if(!config.taskRootFolder || !config.taskId){
62
+ console.log('[ERROR]', 'method resume need param "taskFolder" (or "taskRootFolder" + "taskId")');
63
+ return;
64
+ };
65
+ config.taskFolder = pathUtil.resolve(config.taskRootFolder, config.taskId);
66
+ };
67
+ config.taskRootFolder = pathUtil.dirname(config.taskFolder);
68
+ config.taskId = pathUtil.basename(config.taskFolder);
29
69
 
70
+ config = _mergeSnapshotConfig(config);
71
+ Multithread.start(config);
72
+ };
73
+ const retry_fails = (config)=>{
74
+ config = _normalizeResumeArg('retry_fails', config);
75
+ let taskFolder = _toAbsPath(config.taskFolder);
76
+ if(!taskFolder){
77
+ console.log('[ERROR]', 'method retry_fails need param "taskFolder"');
78
+ return;
79
+ }
80
+ config.taskFolder = taskFolder;
81
+ config.taskRootFolder = pathUtil.dirname(taskFolder);
82
+ config.taskId = pathUtil.basename(taskFolder);
83
+ config.__resume = true;
84
+ config.__retryFails = true;
85
+ config = _mergeSnapshotConfig(config);
30
86
  Multithread.start(config);
31
87
  };
32
88
  const restart = (config)=>{
33
- config = backwardv1.v1tov2(config);
34
- let {taskFolder} = config;
89
+ config = _normalizeResumeArg('restart', config);
90
+ let taskFolder = _toAbsPath(config.taskFolder);
35
91
  if(!taskFolder){
36
92
  console.log('[ERROR]', 'method restart need param "taskFolder"');
37
93
  return;
38
94
  }
95
+ config.taskFolder = taskFolder;
39
96
  config.taskRootFolder = pathUtil.dirname(taskFolder);
40
97
  config.taskId = pathUtil.basename(taskFolder);
41
- config.__resume = true;
42
98
  config.__restart = true;
43
- config.initialTasks = [];
99
+ config = _mergeSnapshotConfig(config);
44
100
  Multithread.start(config);
45
101
  };
46
102
 
@@ -77,7 +133,8 @@ const multiTasks = (config)=>{
77
133
  };
78
134
 
79
135
  multiTasks.start = start;
80
- multiTasks.restart = restart;
136
+ multiTasks.retry_fails = retry_fails;
81
137
  multiTasks.resume = resume;
138
+ multiTasks.restart = restart;
82
139
 
83
140
  module.exports = multiTasks;