multi-tasks 3.1.3 → 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,260 +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 `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.
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 exit (only when `autoCloseAfterCompletion` is `true`); `report` is an empty object for now.
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, //set true to exit the process when all tasks are done; keep it false (default) if you create new tasks dynamically
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
- //Examples 1-3 below are independent variants of processTask, use one at a time.
95
-
96
- //Example1, return a promise for async processes,
97
- // you can return a promise or a non-promise result,
98
- // all result data can be found in the results/succ folder
99
- let processTask = (task, helper)=>{
100
- let {taskCount} = task;
101
-
102
- return new Promise((resolve, reject)=>{
103
- resolve({
104
- data:`task${taskCount} complete`
105
- })
106
- })
107
- };
108
-
109
- //Example2, dynamically create a new task while processing
110
- let processTask = (task, helper)=>{
111
- let {taskCount} = task;
112
-
113
- if(taskCount % 2 === 0){
114
- //create a new task if needed
115
- helper.createNewTasks({
116
- msg:'a new task'
117
- });
118
- return;
119
- }
120
-
121
- return new Promise((resolve, reject)=>{
122
- resolve({
123
- data:`task${taskCount} complete`
124
- })
125
- })
126
- };
127
-
128
- //Example3, generate/throw exceptions in a task method
129
- let processTask = (task, helper)=>{
130
- let {taskCount} = task;
131
-
132
- //Demo of exceptions/errors: they are captured and saved in the results/errors folder
133
- if(taskCount===3) throw 'exception';
134
- if(taskCount===4) return Promise.reject({err:'a test error'});//use Promise.reject method
135
- if(taskCount===5) aaa = bbb;//this ReferenceError will be captured by multi-tasks
136
-
137
- return {data:'succ'};
138
- }
139
-
140
- //Example4, timeout: a task that does not finish within taskTimeout is
141
- // treated as failed with a timeout error
142
- multiTasks({
143
- initialTasks: alltasks,
144
- processTask,
145
- taskRootFolder: `../examples-tmp-data/example-timeout`,
146
- taskId: 'my-task',
147
- numberOfWorkers: 3,
148
- taskTimeout: 30000,
149
- });
150
-
151
- //Example5, auto-retry a failed task with maxTaskRetries
152
- multiTasks({
153
- initialTasks: alltasks,
154
- processTask,
155
- taskRootFolder: `../examples-tmp-data/example-retry`,
156
- taskId: 'my-task',
157
- numberOfWorkers: 3,
158
- maxTaskRetries: 2,
159
- });
160
-
161
- //Example6, broadcast events between workers with helper.emit/helper.setListener
162
- multiTasks({
163
- initialTasks: alltasks,
164
- taskRootFolder: `../examples-tmp-data/example-broadcast`,
165
- taskId: 'my-task',
166
- numberOfWorkers: 3,
167
- processTask: (task, helper) => {
168
- helper.setListener('task-done', (payload, meta) => {
169
- console.log(`worker ${meta.fromWorkerPid} says: task ${payload.seq} done`);
170
- });
171
- helper.emit('task-done', {seq: task.seq});
172
- return {data:'succ'};
173
- },
174
- });
175
-
176
- //Example7, system events from a worker to the master with helper.emitSys
177
- multiTasks({
178
- initialTasks: alltasks,
179
- taskRootFolder: `../examples-tmp-data/example-sysevent`,
180
- taskId: 'my-task',
181
- numberOfWorkers: 3,
182
- setSysListener: (event, payload, meta) => {
183
- console.log(`sys event "${event}" from worker ${meta.fromWorkerPid}:`, payload);
184
- },
185
- processTask: (task, helper) => {
186
- helper.emitSys('task-started', {seq: task.seq});
187
- if(task.fatal){
188
- helper.emitSys('TERMINATE_ALL_WORKERS');//stop everything
189
- return;
190
- };
191
- return {data:'succ'};
192
- },
193
- });
194
-
195
- ```
196
-
197
- ### Resuming
198
-
199
- If the execution is interrupted (e.g. a power outage), resume it like this:
200
-
201
- ```javascript
202
-
203
- multiTasks({
204
- initialTasks: `/myworks/my_scan_tasks/`, //point 'initialTasks' to the interrupted task directory; multi-tasks reads the tasks in its 'new' folder and continues execution
205
- ...
206
- ...//Other configurations remain unchanged
207
- ...
208
- });
209
-
210
- ```
211
-
212
- ### Changelog:
213
-
214
- - 3.1.3 Update README
215
- - 3.1.2 Update README
216
- - 3.1.1 Fix readme documentation
217
- - 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)
218
- - 3.0.4 numberOfWorkers accepts a percentage string of CPU cores, e.g. "50%"
219
- - 3.0.3 Support 'maxTaskRetries'
220
- - 3.0.2 Support 'taskTimeout'
221
- - 3.0.1 Versions 3.0.0 and above are maintained by AI
222
- - 2.2.0 Support Resuming from an interrupted task
223
- - 2.1.2 Update readme
224
- - 2.1.1 Fix: recreate a new one when a worker collapsed unexpectedly.
225
- - 2.1.0 Add a new output directory "log" where you can view the process time of each subtask
226
- - 2.0.9 Update readme examples
227
- - 2.0.8 Update readme examples
228
- - 2.0.7 Update changelog
229
- - 2.0.6 Update readme, remove failed examples
230
- - 2.0.5 Update readme examples
231
- - 2.0.4 Support resume from a failed task
232
- - 2.0.3 Fix: mkdir bug on windows
233
- - 2.0.2 new feature: support shouldTerminate
234
- - 2.0.1 Avoid possible I/O conflicts.
235
- - 2.0.0 Rewritten with a new architecture to support dynamic tasks.
236
- - 1.2.8 Fix: create task folder failed on MacOS
237
- - 1.2.7 Small updates
238
- - 1.2.6 Support onFinish event
239
- - 1.2.5 Rename numberOfWorks to numberOfWorkers, the old one are still supported ;-)
240
- - 1.2.4 Fix: opt.numberOfWorkers not work
241
- - 1.2.3 Update README
242
- - 1.2.2 Update README and examples
243
- - 1.2.1 Handle exceptions and errors in subtasks
244
- - 1.2.0 Simplified usage by providing the function way and support return Promise
245
- - 1.1.4 Remove make-dir
246
- - 1.1.3 Simplified usage, see example0
247
- - 1.1.2
248
- - 1.1.1 Rename files, updated changelog
249
- - 1.1.0 Simplified the usage of a customized Consumer, see example0
250
- - 1.0.8 Fix examples
251
- - 1.0.7 Remove moment
252
- - 1.0.6 Performance optimization
253
-
254
- ### Gitee:
255
-
256
- [https://gitee.com/zhanglei923/multi-tasks](https://gitee.com/zhanglei923/multi-tasks)
257
-
258
- ### License:
259
-
260
- [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.3",
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;