multi-tasks 3.1.7 → 3.1.9
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 +28 -1
- package/package.json +1 -1
- package/workers/Multithread.js +63 -38
- package/workers/index.js +3 -6
package/README.md
CHANGED
|
@@ -10,6 +10,31 @@ Multi-tasks is a toolkit to manage long-term and large-scale parallel computing
|
|
|
10
10
|
npm install multi-tasks
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## 🎉 What's New — Real-Time Monitoring Has Arrived! 🎉
|
|
14
|
+
|
|
15
|
+
**1. Install** the monitor:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install multi-tasks-monitor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**2. Start your multi-tasks run first** — the monitor reads the task folder, which only exists once your run has started. Then either:
|
|
22
|
+
|
|
23
|
+
- Easiest (multi-tasks 3.1.9+): your startup log prints the complete command — full task folder path included. Copy it into another terminal and run:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
****************************************************************
|
|
27
|
+
[Master]: monitor this task with:
|
|
28
|
+
npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
|
|
29
|
+
****************************************************************
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Or just run `npx multi-tasks-monitor` and pick the task folder on the page with "Select Directory".
|
|
33
|
+
|
|
34
|
+
**3. Watch the monitor page.** The live dashboard opens in your browser automatically — and you can revisit http://localhost:3777 anytime. 🚀
|
|
35
|
+
|
|
36
|
+
Running multi-tasks older than 3.1.9, or want the full command-line options? See the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
|
|
37
|
+
|
|
13
38
|
### API:
|
|
14
39
|
|
|
15
40
|
The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
|
|
@@ -40,7 +65,7 @@ The API has three parts: the entry functions, the config options, and the `helpe
|
|
|
40
65
|
|
|
41
66
|
**Other options:**
|
|
42
67
|
|
|
43
|
-
- **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`.
|
|
68
|
+
- **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`/`restart`.
|
|
44
69
|
|
|
45
70
|
**Task helper** (the `helper` object passed as the second argument of `processTask`):
|
|
46
71
|
|
|
@@ -272,6 +297,8 @@ Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **
|
|
|
272
297
|
|
|
273
298
|
### Changelog:
|
|
274
299
|
|
|
300
|
+
- 3.1.9 Print the multi-tasks-monitor command on startup
|
|
301
|
+
- 3.1.8 Small refinements
|
|
275
302
|
- 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
|
|
276
303
|
- 3.1.6 Fix silent task loss under heavy load with maxTaskRetries
|
|
277
304
|
- 3.1.5 Update README
|
package/package.json
CHANGED
package/workers/Multithread.js
CHANGED
|
@@ -158,49 +158,64 @@ const start = (config)=>{
|
|
|
158
158
|
}
|
|
159
159
|
if(message === MSG_REQUEST_NEW_TASK){
|
|
160
160
|
clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
|
|
161
|
-
queue.enqueue(async ()=>{
|
|
162
|
-
return TaskMgr.updateNewTasks();
|
|
163
|
-
});//use queue to make sure no I/O conflict
|
|
164
161
|
queue.enqueue(async () => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
subTask
|
|
189
|
-
},()=>{}, {
|
|
162
|
+
//use queue to make sure no I/O conflict; update/pop/判空须在同一个串行任务里原子完成
|
|
163
|
+
await TaskMgr.updateNewTasks();
|
|
164
|
+
let subTask = await TaskMgr.popNewTask();
|
|
165
|
+
if(!subTask){
|
|
166
|
+
//判空前强制绕开缓存重读一次磁盘再 pop: 防其他 worker 动态建任务(createNewTasks)
|
|
167
|
+
//落在 update 与判空之间被漏判, 导致 autoClose 提前杀光 worker / 丢新任务
|
|
168
|
+
await TaskMgr.loadNewTasks();
|
|
169
|
+
subTask = await TaskMgr.popNewTask();
|
|
170
|
+
}
|
|
171
|
+
if(!subTask && config.autoCloseAfterCompletion){
|
|
172
|
+
//杀 worker 前延时 1s 再兜底确认一次: 等其他 worker 可能正在落盘的动态新任务
|
|
173
|
+
//(best-effort 再压小竞态窗口, 只限 autoClose 路径, 普通派发零开销)
|
|
174
|
+
await new Promise((resolve)=>{ setTimeout(resolve, 1000); });
|
|
175
|
+
await TaskMgr.loadNewTasks();
|
|
176
|
+
subTask = await TaskMgr.popNewTask();
|
|
177
|
+
}
|
|
178
|
+
if(!subTask){
|
|
179
|
+
//no tasks left
|
|
180
|
+
console.log('[Master]: no tasks left!');
|
|
181
|
+
if(config.autoCloseAfterCompletion){
|
|
182
|
+
killWorker(worker);
|
|
183
|
+
}else{//waiting for new tasks
|
|
184
|
+
worker.send(MSG_NO_TASK_FOUND,()=>{}, {
|
|
190
185
|
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
191
186
|
});
|
|
192
|
-
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
193
|
-
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
194
|
-
if(hasTimeout || hasRetry){
|
|
195
|
-
workerTaskTimers[worker.id] = {
|
|
196
|
-
timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
|
|
197
|
-
subTask,
|
|
198
|
-
startTimestamp
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
taskCount++;
|
|
202
187
|
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
let terminateData = {taskCount,subTask,config, id: worker.id};
|
|
191
|
+
if(USER_CONFIG.shouldTerminate && USER_CONFIG.shouldTerminate(terminateData)===true) {
|
|
192
|
+
killAllWorkers();
|
|
193
|
+
return process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
//send new tasks to the worker
|
|
196
|
+
let startTimestamp = new Date()*1;
|
|
197
|
+
//IPC 用裁剪副本: worker 端只取目录路径, 不需要 initialTasks 全量数组;
|
|
198
|
+
//带着它每次派发都要 O(N) 序列化(总量 O(N²))。config 本体保留: resume/restart 重写快照要用
|
|
199
|
+
let ipcConfig = { ...config };
|
|
200
|
+
delete ipcConfig.initialTasks;
|
|
201
|
+
worker.send({
|
|
202
|
+
taskCount,
|
|
203
|
+
startTimestamp,
|
|
204
|
+
config: ipcConfig,
|
|
205
|
+
subTask
|
|
206
|
+
},()=>{}, {
|
|
207
|
+
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
203
208
|
});
|
|
209
|
+
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
210
|
+
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
211
|
+
if(hasTimeout || hasRetry){
|
|
212
|
+
workerTaskTimers[worker.id] = {
|
|
213
|
+
timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
|
|
214
|
+
subTask,
|
|
215
|
+
startTimestamp
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
taskCount++;
|
|
204
219
|
});
|
|
205
220
|
}
|
|
206
221
|
});
|
|
@@ -210,10 +225,20 @@ const start = (config)=>{
|
|
|
210
225
|
initPromise.then((updated_config)=>{
|
|
211
226
|
console.log(` new tasks=${TaskMgr.getTasks('new').length}`);
|
|
212
227
|
config = updated_config;
|
|
228
|
+
console.log('');//以下: 配套监控工程命令提示, 仅 master 打印一次, 星号边框突出显示
|
|
229
|
+
console.log('****************************************************************');
|
|
230
|
+
console.log('[Master]: monitor this task with:');
|
|
231
|
+
console.log(` npx multi-tasks-monitor --task-dir "${config.taskFolder}" --port 3777`);
|
|
232
|
+
console.log('****************************************************************');
|
|
213
233
|
for (let i = 0; i < numOfWorkers; i++) {
|
|
214
234
|
let worker = cluster.fork();
|
|
215
235
|
initWorker(worker);
|
|
216
236
|
}
|
|
237
|
+
}).catch((e)=>{
|
|
238
|
+
//init/resume 异步失败(rename/读盘错误等): 明确 FATAL + 非 0 退出,
|
|
239
|
+
//否则 unhandled rejection 在 warn 模式下会不 fork 不退出、静默挂死
|
|
240
|
+
console.error('[FATAL] failed to init tasks:', e);
|
|
241
|
+
process.exit(1);
|
|
217
242
|
});
|
|
218
243
|
cluster.on('exit', (worker, code, signal) => {
|
|
219
244
|
let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
|
package/workers/index.js
CHANGED
|
@@ -59,8 +59,7 @@ const resume = (config)=>{
|
|
|
59
59
|
config.taskFolder = _toAbsPath(config.taskFolder);
|
|
60
60
|
if(!config.taskFolder){
|
|
61
61
|
if(!config.taskRootFolder || !config.taskId){
|
|
62
|
-
|
|
63
|
-
return;
|
|
62
|
+
throw new Error('method resume need param "taskFolder" (or "taskRootFolder" + "taskId")');
|
|
64
63
|
};
|
|
65
64
|
config.taskFolder = pathUtil.resolve(config.taskRootFolder, config.taskId);
|
|
66
65
|
};
|
|
@@ -74,8 +73,7 @@ const retry_fails = (config)=>{
|
|
|
74
73
|
config = _normalizeResumeArg('retry_fails', config);
|
|
75
74
|
let taskFolder = _toAbsPath(config.taskFolder);
|
|
76
75
|
if(!taskFolder){
|
|
77
|
-
|
|
78
|
-
return;
|
|
76
|
+
throw new Error('method retry_fails need param "taskFolder"');
|
|
79
77
|
}
|
|
80
78
|
config.taskFolder = taskFolder;
|
|
81
79
|
config.taskRootFolder = pathUtil.dirname(taskFolder);
|
|
@@ -89,8 +87,7 @@ const restart = (config)=>{
|
|
|
89
87
|
config = _normalizeResumeArg('restart', config);
|
|
90
88
|
let taskFolder = _toAbsPath(config.taskFolder);
|
|
91
89
|
if(!taskFolder){
|
|
92
|
-
|
|
93
|
-
return;
|
|
90
|
+
throw new Error('method restart need param "taskFolder"');
|
|
94
91
|
}
|
|
95
92
|
config.taskFolder = taskFolder;
|
|
96
93
|
config.taskRootFolder = pathUtil.dirname(taskFolder);
|