multi-tasks 3.3.0 → 3.4.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 +6 -5
- package/package.json +36 -36
- package/utils/makedir.js +12 -18
- package/utils/progressCtl.js +287 -187
- package/utils/progressView.js +110 -102
- package/utils/randoms.js +5 -2
- package/utils/startupBanner.js +2 -1
- package/utils/workerStatusProbe.js +38 -0
- package/workers/Multithread.js +497 -495
- package/workers/TaskMgr.js +26 -0
- package/workers/WorkerMgr.js +18 -27
package/workers/Multithread.js
CHANGED
|
@@ -1,496 +1,498 @@
|
|
|
1
|
-
const cluster = require('cluster');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const numCPUs = os.cpus().length;
|
|
4
|
-
|
|
5
|
-
let MSG_REQUEST_NEW_TASK = 'request_new_task';
|
|
6
|
-
let MSG_NO_TASK_FOUND = 'no_task_found';
|
|
7
|
-
|
|
8
|
-
const asMaster = require('./asMaster');
|
|
9
|
-
const TaskMgr = require('./TaskMgr');
|
|
10
|
-
const WorkerMgr = require('./WorkerMgr');
|
|
11
|
-
const helper = require('./helper');
|
|
12
|
-
const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
|
|
13
|
-
const { resolveWorkerPriority } = require('../utils/resolvePriority');
|
|
14
|
-
const runLog = require('../utils/runLog');
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
TaskMgr.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
let
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if(!subTask){
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
progressCtl.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
subTask.
|
|
386
|
-
subTask.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
serialized = e;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
serialized
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
let
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
resultPromise
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}).
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
let
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
let
|
|
456
|
-
if(hasError
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
};
|
|
1
|
+
const cluster = require('cluster');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const numCPUs = os.cpus().length;
|
|
4
|
+
|
|
5
|
+
let MSG_REQUEST_NEW_TASK = 'request_new_task';
|
|
6
|
+
let MSG_NO_TASK_FOUND = 'no_task_found';
|
|
7
|
+
|
|
8
|
+
const asMaster = require('./asMaster');
|
|
9
|
+
const TaskMgr = require('./TaskMgr');
|
|
10
|
+
const WorkerMgr = require('./WorkerMgr');
|
|
11
|
+
const helper = require('./helper');
|
|
12
|
+
const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
|
|
13
|
+
const { resolveWorkerPriority } = require('../utils/resolvePriority');
|
|
14
|
+
const runLog = require('../utils/runLog');
|
|
15
|
+
const randomsUtil = require('../utils/randoms');
|
|
16
|
+
const progressCtl = require('../utils/progressCtl');
|
|
17
|
+
const workerStatusProbe = require('../utils/workerStatusProbe');
|
|
18
|
+
const startupBanner = require('../utils/startupBanner');
|
|
19
|
+
|
|
20
|
+
//时间戳统一走 utils/randoms.getDateTimeTxt(YYYY-MM-DD_HH-mm-ss), 与 asMaster/user_messages 同口径
|
|
21
|
+
|
|
22
|
+
let USER_CONFIG;
|
|
23
|
+
|
|
24
|
+
let taskCount = 0;
|
|
25
|
+
|
|
26
|
+
let workerTaskTimers = {};//worker.id -> {timer, subTask, startTimestamp}, master 端任务超时倒计时
|
|
27
|
+
|
|
28
|
+
let killAllWorkers = ()=>{
|
|
29
|
+
for (const id in cluster.workers) {
|
|
30
|
+
if (cluster.workers.hasOwnProperty(id)) {
|
|
31
|
+
const worker = cluster.workers[id];
|
|
32
|
+
runLog.write(`Killing worker ${worker.process.pid}`);
|
|
33
|
+
killWorker(worker);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
let killWorker = (worker)=>{
|
|
38
|
+
worker.disconnect();
|
|
39
|
+
worker.kill();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const start = (config)=>{
|
|
43
|
+
USER_CONFIG = config;//this will remain user defined functions
|
|
44
|
+
let isMaster = (cluster.isPrimary || cluster.isMaster);
|
|
45
|
+
|
|
46
|
+
if (isMaster) {//init
|
|
47
|
+
//同进程再跑一轮 multiTasks 时复位 progressBar 控制器状态,
|
|
48
|
+
//否则残留会让新一轮(不带 progressBar)继续渲染旧进度区
|
|
49
|
+
progressCtl.reset();
|
|
50
|
+
runLog.reset();//runLog 已脱离 progressCtl: 不复位则新一轮日志继续写旧 run.log、console 仍被旧 hook 拦截
|
|
51
|
+
config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串; 须在 fork 循环读值前折算, 非法值在此抛错
|
|
52
|
+
config.workerPriority = resolveWorkerPriority(config.workerPriority);//枚举映射/数字校验; 非法值 fail fast(同 numberOfWorkers)
|
|
53
|
+
let numOfWorkers = config.numberOfWorkers;
|
|
54
|
+
|
|
55
|
+
const AsyncQueue = require(`../utils/asyncQueue`);
|
|
56
|
+
const queue = new AsyncQueue();
|
|
57
|
+
|
|
58
|
+
const masterStartTimestamp = new Date()*1;//本轮 master 启动时间(resume 不追溯首轮)
|
|
59
|
+
let workerCrashCount = 0;//累计崩溃补员次数, 进 onFinish report
|
|
60
|
+
|
|
61
|
+
let clearTaskTimer = (workerId)=>{
|
|
62
|
+
let rec = workerTaskTimers[workerId];
|
|
63
|
+
if(rec){
|
|
64
|
+
clearTimeout(rec.timer);
|
|
65
|
+
delete workerTaskTimers[workerId];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
let terminateAllAndExit = ()=>{
|
|
69
|
+
//shouldTerminate 与 TERMINATE_ALL_WORKERS 共用: 清在飞任务计时器 + 杀 worker + 进度条定格退出
|
|
70
|
+
for (const id in workerTaskTimers) {
|
|
71
|
+
clearTaskTimer(id);
|
|
72
|
+
};
|
|
73
|
+
killAllWorkers();
|
|
74
|
+
//计数器行与正常完成路径一致: 始终进 run.log(progressBar 未接管终端时 echo 上屏);
|
|
75
|
+
//runLog 写流与 stdout 都是异步写, 先写完再延迟 exit, 防计数器行被 process.exit 截断
|
|
76
|
+
let finalStats = TaskMgr.getFinalStats();
|
|
77
|
+
runLog.write(`[Tasks] succeeded=${finalStats.succeeded}, failed=${finalStats.failed}, succ-but-save-failed=${finalStats.succButSaveFailed || 0}`);
|
|
78
|
+
progressCtl.finish();
|
|
79
|
+
setTimeout(()=>{
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}, 10);
|
|
82
|
+
};
|
|
83
|
+
let finalizeTimeoutTask = (subTask, cost)=>{
|
|
84
|
+
let subid = subTask.subid;
|
|
85
|
+
try{
|
|
86
|
+
let resultType = TaskMgr.getSavedResultType(subid);
|
|
87
|
+
if(resultType === 'succ'){//超时边界: worker 已成功写盘, 视为已完成
|
|
88
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished');
|
|
89
|
+
}else if(resultType === 'error'){//超时边界: worker 已自行记错误
|
|
90
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished_with_errors');
|
|
91
|
+
}else{
|
|
92
|
+
if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
93
|
+
let retryCount = TaskMgr.incrementTaskRetry(subid);
|
|
94
|
+
if(retryCount <= config.maxTaskRetries){
|
|
95
|
+
runLog.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
96
|
+
TaskMgr.mvTaskSync(subid, 'running', 'new');
|
|
97
|
+
return;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
TaskMgr.saveTaskResult(subid, true, [{type: 'timeout', timeout: config.taskTimeout, cost}]);
|
|
101
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished_with_errors');
|
|
102
|
+
}
|
|
103
|
+
}catch(e){
|
|
104
|
+
runLog.warn('[Master]: failed to finalize timeout task', subid, e);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
let onTaskTimeout = (worker)=>{
|
|
108
|
+
let rec = workerTaskTimers[worker.id];
|
|
109
|
+
if(!rec) return;
|
|
110
|
+
delete workerTaskTimers[worker.id];
|
|
111
|
+
let {subTask, startTimestamp} = rec;
|
|
112
|
+
let cost = new Date()*1 - startTimestamp;
|
|
113
|
+
runLog.warn(`[Master]: task "${subTask.subid}" timeout after ${cost}ms, killing worker pid=${worker.process.pid}`);
|
|
114
|
+
worker.kill();//不 disconnect: exit 事件会按崩溃补员
|
|
115
|
+
finalizeTimeoutTask(subTask, cost);
|
|
116
|
+
};
|
|
117
|
+
let handleCrashedTask = (subTask)=>{
|
|
118
|
+
if(!(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0)) return;
|
|
119
|
+
let subid = subTask.subid;
|
|
120
|
+
try{
|
|
121
|
+
let retryCount = TaskMgr.incrementTaskRetry(subid);
|
|
122
|
+
if(retryCount <= config.maxTaskRetries){
|
|
123
|
+
runLog.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
|
|
124
|
+
TaskMgr.mvTaskSync(subid, 'running', 'new');
|
|
125
|
+
}else{
|
|
126
|
+
runLog.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
|
|
127
|
+
TaskMgr.saveTaskResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
|
|
128
|
+
TaskMgr.mvTaskSync(subid, 'running', 'finished_with_errors');
|
|
129
|
+
};
|
|
130
|
+
}catch(e){
|
|
131
|
+
runLog.warn('[Master]: failed to handle crashed task', subid, e);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
let initWorker = (worker)=>{
|
|
136
|
+
WorkerMgr.addWorker(worker);
|
|
137
|
+
if (typeof config.workerPriority === 'number') {
|
|
138
|
+
try {
|
|
139
|
+
os.setPriority(worker.process.pid, config.workerPriority);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
//优先级是建议性设置: 失败(如 POSIX 上调高需 root)仅告警, 不中断批任务
|
|
142
|
+
runLog.warn(`[Master]: failed to set priority of worker pid=${worker.process.pid}`, e);
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
worker.on('message', function(message) {
|
|
146
|
+
if(message && message.__workerStatus){//worker 状态周期自报: master 自己消费进状态表, 不转发 setSysListener, 不触发派发
|
|
147
|
+
progressCtl.workerStatus(worker.id, message.__workerStatus);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if(message && message.__sysEvent){//worker 系统事件: master 自己消费, 不转发
|
|
151
|
+
if(message.event === 'TERMINATE_ALL_WORKERS'){
|
|
152
|
+
runLog.warn(`[Master]: TERMINATE_ALL_WORKERS from worker ${worker.id}, killing all workers`);
|
|
153
|
+
return terminateAllAndExit();
|
|
154
|
+
}
|
|
155
|
+
let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
|
|
156
|
+
if(typeof USER_CONFIG.setSysListener === 'function'){
|
|
157
|
+
try{
|
|
158
|
+
USER_CONFIG.setSysListener(message.event, message.payload, meta);
|
|
159
|
+
}catch(e){
|
|
160
|
+
runLog.error(`[Master]: setSysListener for event "${message.event}" threw:`, e);
|
|
161
|
+
}
|
|
162
|
+
}else{
|
|
163
|
+
runLog.warn(`[Master]: unknown sys event "${message.event}" from worker ${worker.id} (no setSysListener configured)`);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if(message && message.__broadcast){//worker 广播请求: master 中转给所有 worker
|
|
168
|
+
let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
|
|
169
|
+
for (const id in cluster.workers) {
|
|
170
|
+
let w = cluster.workers[id];
|
|
171
|
+
if(w.id === worker.id && message.includingMe === false) continue;
|
|
172
|
+
try{
|
|
173
|
+
w.send({__broadcast: true, event: message.event, payload: message.payload, meta});
|
|
174
|
+
}catch(e){
|
|
175
|
+
runLog.warn(`[Master]: failed to relay broadcast to worker ${w.id}`, e);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if(message === MSG_REQUEST_NEW_TASK){
|
|
181
|
+
clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
|
|
182
|
+
progressCtl.taskCompleted(worker.id);//worker 行回到 idle; 完成数按盘上结果目录统计, 不在此计数
|
|
183
|
+
queue.enqueue(async () => {
|
|
184
|
+
//use queue to make sure no I/O conflict; update/pop/判空须在同一个串行任务里原子完成
|
|
185
|
+
await TaskMgr.updateNewTasks();
|
|
186
|
+
let subTask = await TaskMgr.popNewTask();
|
|
187
|
+
if(!subTask){
|
|
188
|
+
//判空前强制绕开缓存重读一次磁盘再 pop: 防其他 worker 动态建任务(createNewTasks)
|
|
189
|
+
//落在 update 与判空之间被漏判, 导致 autoClose 提前杀光 worker / 丢新任务
|
|
190
|
+
await TaskMgr.loadNewTasks();
|
|
191
|
+
subTask = await TaskMgr.popNewTask();
|
|
192
|
+
}
|
|
193
|
+
if(!subTask && config.autoCloseAfterCompletion){
|
|
194
|
+
//杀 worker 前延时 1s 再兜底确认一次: 等其他 worker 可能正在落盘的动态新任务
|
|
195
|
+
//(best-effort 再压小竞态窗口, 只限 autoClose 路径, 普通派发零开销)
|
|
196
|
+
await new Promise((resolve)=>{ setTimeout(resolve, 1000); });
|
|
197
|
+
await TaskMgr.loadNewTasks();
|
|
198
|
+
subTask = await TaskMgr.popNewTask();
|
|
199
|
+
}
|
|
200
|
+
if(!subTask){
|
|
201
|
+
//no tasks left
|
|
202
|
+
runLog.write('[Master]: no tasks left!');
|
|
203
|
+
if(config.autoCloseAfterCompletion){
|
|
204
|
+
killWorker(worker);
|
|
205
|
+
}else{//waiting for new tasks
|
|
206
|
+
worker.send(MSG_NO_TASK_FOUND,()=>{});
|
|
207
|
+
}
|
|
208
|
+
progressCtl.render();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let terminateData = {taskCount,subTask,config, id: worker.id};
|
|
212
|
+
let userWantsTerminate = false;
|
|
213
|
+
if(typeof USER_CONFIG.shouldTerminate === 'function'){
|
|
214
|
+
try{
|
|
215
|
+
userWantsTerminate = USER_CONFIG.shouldTerminate(terminateData)===true;
|
|
216
|
+
}catch(e){
|
|
217
|
+
//任务已 pop 就必须派出去: 抛错只记日志, 按「不终止」继续(裸 throw 会丢任务并死锁)
|
|
218
|
+
runLog.error('[Master]: shouldTerminate threw, continue dispatching task:', e);
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
if(userWantsTerminate) {
|
|
222
|
+
return terminateAllAndExit();
|
|
223
|
+
}
|
|
224
|
+
//send new tasks to the worker
|
|
225
|
+
let startTimestamp = new Date()*1;
|
|
226
|
+
//IPC 用裁剪副本: worker 端只取目录路径, 不需要 initialTasks 全量数组;
|
|
227
|
+
//带着它每次派发都要 O(N) 序列化(总量 O(N²))。config 本体保留: resume/restart 重写快照要用
|
|
228
|
+
let ipcConfig = { ...config };
|
|
229
|
+
delete ipcConfig.initialTasks;
|
|
230
|
+
worker.send({
|
|
231
|
+
taskCount,
|
|
232
|
+
startTimestamp,
|
|
233
|
+
config: ipcConfig,
|
|
234
|
+
subTask
|
|
235
|
+
},()=>{});
|
|
236
|
+
progressCtl.taskAssigned(worker.id, subTask.subid);
|
|
237
|
+
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
238
|
+
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
239
|
+
if(hasTimeout || hasRetry){
|
|
240
|
+
workerTaskTimers[worker.id] = {
|
|
241
|
+
timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
|
|
242
|
+
subTask,
|
|
243
|
+
startTimestamp
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
taskCount++;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
let initPromise = config.__resume ? asMaster.resume(config) : asMaster.init(config);
|
|
253
|
+
initPromise.then((updated_config)=>{
|
|
254
|
+
config = updated_config;
|
|
255
|
+
//一次性启动块(走 console, 在劫持前上屏): 标题框横幅 + 平铺关键字段, 模式由 __ 标记推导;
|
|
256
|
+
//init 成功后所有信息齐备(new tasks 数/masterTaskId/__sysLogFile), 一次打完
|
|
257
|
+
let startupInfo = {
|
|
258
|
+
mode: config.__restart ? 'RESTART' : (config.__retryFails ? 'RETRY_FAILS' : (config.__resume ? 'RESUME' : 'START')),
|
|
259
|
+
taskId: config.masterTaskId,
|
|
260
|
+
taskFolder: config.taskFolder,
|
|
261
|
+
numOfWorkers,
|
|
262
|
+
numCPUs,
|
|
263
|
+
pid: process.pid,
|
|
264
|
+
newTasks: TaskMgr.getTasks('new').length,
|
|
265
|
+
sysLogFile: config.__sysLogFile,
|
|
266
|
+
startedAt: randomsUtil.getDateTimeTxt(),
|
|
267
|
+
};
|
|
268
|
+
console.log(startupBanner.renderBanner(startupInfo));
|
|
269
|
+
progressCtl.setup({//TTY 判定 + 渲染器创建都在控制器内; 统计口径注入, 控制器不依赖 TaskMgr
|
|
270
|
+
config,
|
|
271
|
+
stream: process.stdout,
|
|
272
|
+
getStats: () => TaskMgr.getFinalStats(),
|
|
273
|
+
getRemaining: () => TaskMgr.getTasks('new').length,
|
|
274
|
+
});
|
|
275
|
+
//console 劫持写 run.log 是永久机制, 与 progressBar 无关: 进度区接管终端(开且 TTY)时只写文件,
|
|
276
|
+
//否则 echo 同时透传 console 上屏; 判定收敛在 initAndHook 内, 无 __sysLogFile 时维持 console
|
|
277
|
+
runLog.initAndHook(config.__sysLogFile, { echo: !progressCtl.isOn() });
|
|
278
|
+
startupBanner.writeLogHeader(startupInfo);//同一组关键信息写 run.log 头部; 无 __sysLogFile 时内部 no-op
|
|
279
|
+
for (let i = 0; i < numOfWorkers; i++) {
|
|
280
|
+
let worker = cluster.fork();
|
|
281
|
+
initWorker(worker);
|
|
282
|
+
progressCtl.workerStarted(worker.id, worker.process.pid);
|
|
283
|
+
}
|
|
284
|
+
progressCtl.render();
|
|
285
|
+
}).catch((e)=>{
|
|
286
|
+
//init/resume 异步失败(rename/读盘错误等): 明确 FATAL + 非 0 退出,
|
|
287
|
+
//否则 unhandled rejection 在 warn 模式下会不 fork 不退出、静默挂死
|
|
288
|
+
console.error('[FATAL] failed to init tasks:', e);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|
|
291
|
+
cluster.on('exit', (worker, code, signal) => {
|
|
292
|
+
let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
|
|
293
|
+
clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
|
|
294
|
+
progressCtl.workerExited(worker.id);
|
|
295
|
+
WorkerMgr.removeWorker(worker.id);//清注册表引用: 崩溃补员场景 map 只增不减会累积死 worker
|
|
296
|
+
if (code !== 0 && !worker.exitedAfterDisconnect) {
|
|
297
|
+
runLog.write('Worker died unexpectedly starting a new one...');
|
|
298
|
+
workerCrashCount++;
|
|
299
|
+
if(rec && rec.subTask){
|
|
300
|
+
handleCrashedTask(rec.subTask);
|
|
301
|
+
};
|
|
302
|
+
let worker = cluster.fork();
|
|
303
|
+
initWorker(worker);
|
|
304
|
+
progressCtl.workerStarted(worker.id, worker.process.pid);
|
|
305
|
+
progressCtl.render();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let allkilled = true;
|
|
309
|
+
for (const worker of Object.values(cluster.workers)) {
|
|
310
|
+
allkilled = allkilled && worker.killed;
|
|
311
|
+
}
|
|
312
|
+
if(allkilled){
|
|
313
|
+
if(USER_CONFIG.onFinish){
|
|
314
|
+
let endTimestamp = new Date()*1;
|
|
315
|
+
let stats = TaskMgr.getFinalStats();
|
|
316
|
+
let report = {
|
|
317
|
+
taskId: config.masterTaskId,
|
|
318
|
+
taskFolder: stats.taskFolder,
|
|
319
|
+
startTimestamp: masterStartTimestamp,
|
|
320
|
+
endTimestamp,
|
|
321
|
+
cost: endTimestamp - masterStartTimestamp,
|
|
322
|
+
total: stats.succeeded + stats.failed,
|
|
323
|
+
succeeded: stats.succeeded,
|
|
324
|
+
failed: stats.failed,
|
|
325
|
+
failedTasks: stats.failedTasks,
|
|
326
|
+
succButSaveFailed: stats.succButSaveFailed || 0,//任务成功但结果保存失败(序列化兜底)的任务数, 明细见 .sys/user_messages
|
|
327
|
+
workers: {count: numOfWorkers, crashed: workerCrashCount},
|
|
328
|
+
};
|
|
329
|
+
let onFinishHelper = {
|
|
330
|
+
foreachSuccResult: (cb) => TaskMgr.forEachSuccResult(cb),
|
|
331
|
+
foreachErrorResult: (cb) => TaskMgr.forEachErrorResult(cb),
|
|
332
|
+
foreachResult: (cb) => TaskMgr.forEachResult(cb),
|
|
333
|
+
};
|
|
334
|
+
USER_CONFIG.onFinish(report, onFinishHelper);
|
|
335
|
+
delete USER_CONFIG.onFinish;
|
|
336
|
+
}
|
|
337
|
+
//计数器行先进 run.log(progressBar 未接管终端时 echo 上屏, 接管时由 progressCtl.finish() 打在屏幕);
|
|
338
|
+
//stdout/run.log 都是异步写, 写完留 10ms 再 exit, 防 process.exit 截断未 flush 的输出
|
|
339
|
+
let finalStats = TaskMgr.getFinalStats();
|
|
340
|
+
runLog.write(`[Tasks] succeeded=${finalStats.succeeded}, failed=${finalStats.failed}, succ-but-save-failed=${finalStats.succButSaveFailed || 0}`);
|
|
341
|
+
runLog.write('[Master]: all workers are done, process exit.');
|
|
342
|
+
progressCtl.finish();//单行进度条换行定格 + 计数器行, 别让 shell 提示符盖住它
|
|
343
|
+
setTimeout(()=>{
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}, 10);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}else{
|
|
349
|
+
subWorker(config);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
function subWorker(config){
|
|
353
|
+
let worker = cluster.worker;
|
|
354
|
+
//worker 进程独立重跑入口, config.__sysLogFile 已由 workers/index.js 入口算好(与 master 同一路径):
|
|
355
|
+
//console 劫持写 run.log 永久生效, 与 progressBar 无关; progressBar 未显式关闭(缺省默认开启)且
|
|
356
|
+
//共享 stdout 为 TTY 时只写文件(与 master 判定一致), 否则 echo 同时上屏。判定收敛在 initAndHook 内,
|
|
357
|
+
//无路径时维持 console。入口即生效, 领不到任务的富余 worker 的 waiting 日志同样进 run.log。
|
|
358
|
+
runLog.initAndHook(config.__sysLogFile, { echo: !(config.progressBar !== false && process.stdout.isTTY) });
|
|
359
|
+
//状态探针: 启动条件与上面 echo 判定同口径(progressBar 开 + TTY), 另需 interval > 0(缺省 2000);
|
|
360
|
+
//非 TTY/关闭时不启动, 零开销零 IPC 噪音。探针 unref, 进程退出随进程销毁, 无需 stop
|
|
361
|
+
//布尔口径: false 等价 <=0 关闭, true 等价缺省(勿直接进 setInterval, true 会被当 1ms)
|
|
362
|
+
let statusInterval = config.worker_status_poll_ms;
|
|
363
|
+
statusInterval = (typeof statusInterval === 'undefined' || statusInterval === true) ? 2000 : (statusInterval === false ? 0 : statusInterval);
|
|
364
|
+
if(config.progressBar !== false && process.stdout.isTTY && statusInterval > 0){
|
|
365
|
+
workerStatusProbe.start(statusInterval, (payload)=>{
|
|
366
|
+
worker.send({__workerStatus: payload});
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
worker.on('message', function(message) {
|
|
370
|
+
if(message && message.__broadcast){//master 中转来的广播: 分发给 helper.on 注册的 handler
|
|
371
|
+
helper.__dispatchBroadcast(message);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if(message === MSG_NO_TASK_FOUND){//wait for new task
|
|
375
|
+
runLog.write(` (Worker #${worker.id}): waiting`);
|
|
376
|
+
setTimeout(()=>{
|
|
377
|
+
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
378
|
+
}, 5000 + Math.random()*5000);
|
|
379
|
+
}else
|
|
380
|
+
if(message.subTask){//new task
|
|
381
|
+
let {config, subTask, startTimestamp, taskCount} = message;
|
|
382
|
+
TaskMgr.load(config);
|
|
383
|
+
//用户任务自带的 index/taskCount 保留原值(重试/续跑后任务身份不变),
|
|
384
|
+
//未提供时才填全局派发计数器(v1 兼容)
|
|
385
|
+
if(typeof subTask.index === 'undefined') subTask.index = taskCount;
|
|
386
|
+
if(typeof subTask.taskCount === 'undefined') subTask.taskCount = taskCount;
|
|
387
|
+
subTask.workerId = worker.id;
|
|
388
|
+
subTask.workerPid = process.pid;
|
|
389
|
+
runLog.write(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, randomsUtil.getDateTimeTxt());
|
|
390
|
+
let subTaskLog = {
|
|
391
|
+
workerId: worker.id,
|
|
392
|
+
workerPid: process.pid,
|
|
393
|
+
taskCount,
|
|
394
|
+
startTimestamp
|
|
395
|
+
};
|
|
396
|
+
runTask(config, subTask, subTaskLog).then((tasklog)=>{
|
|
397
|
+
if(tasklog.retried) return;//重试任务已回 new 等重跑, 未完成, 不打 [Task Finished]
|
|
398
|
+
let {cost, startTimestamp, endTimestamp} = tasklog;
|
|
399
|
+
|
|
400
|
+
runLog.write(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, randomsUtil.getDateTimeTxt(), `cost=${cost}ms`)
|
|
401
|
+
}).then(()=>{
|
|
402
|
+
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
403
|
+
});
|
|
404
|
+
}else{
|
|
405
|
+
runLog.write('Can not find: message.subTask');
|
|
406
|
+
throw new Error('can not find subTask!')
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
410
|
+
|
|
411
|
+
};
|
|
412
|
+
//错误对象 → 可 JSON 序列化的持久化形态: Error 取 message/stack, 其余原样;
|
|
413
|
+
//错误值不可 JSON 序列化(循环引用/BigInt 等)时换成机器可读占位, 保住「用户 reject 了什么」的语义
|
|
414
|
+
function serializeError(e){
|
|
415
|
+
let serialized;
|
|
416
|
+
if(e instanceof Error){
|
|
417
|
+
serialized = {message: e.message, stack: e.stack};
|
|
418
|
+
}else{
|
|
419
|
+
serialized = e;
|
|
420
|
+
};
|
|
421
|
+
try{
|
|
422
|
+
JSON.stringify(serialized);
|
|
423
|
+
}catch(stringifyErr){
|
|
424
|
+
serialized = {type: 'unserializable_error_value', message: 'error value is not JSON-serializable'};
|
|
425
|
+
};
|
|
426
|
+
return serialized;
|
|
427
|
+
};
|
|
428
|
+
function runTask(config, subTask, subTaskLog){
|
|
429
|
+
return new Promise((resolve)=>{
|
|
430
|
+
let resultPromise = Promise.resolve({});
|
|
431
|
+
let resultData;
|
|
432
|
+
let errors = [];
|
|
433
|
+
helper.load(config);
|
|
434
|
+
TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
|
|
435
|
+
try{
|
|
436
|
+
resultPromise = USER_CONFIG.processTask(subTask, helper);//rewrite this method in subclass
|
|
437
|
+
}catch(e){
|
|
438
|
+
errors.push({subTask, exception: serializeError(e)});
|
|
439
|
+
}
|
|
440
|
+
let ispromise = resultPromise instanceof Promise;
|
|
441
|
+
if(!ispromise){resultPromise = Promise.resolve(resultPromise);}//convert data to promise
|
|
442
|
+
resultPromise.then((data)=>{
|
|
443
|
+
resultData = data;
|
|
444
|
+
}).catch((ex)=>{
|
|
445
|
+
errors.push({ex: serializeError(ex)});
|
|
446
|
+
}).finally(async ()=>{
|
|
447
|
+
let {startTimestamp} = subTaskLog;
|
|
448
|
+
let endTimestamp = new Date()*1;
|
|
449
|
+
let cost = endTimestamp - startTimestamp;
|
|
450
|
+
subTaskLog = {
|
|
451
|
+
...subTaskLog,
|
|
452
|
+
endTimestamp,
|
|
453
|
+
cost
|
|
454
|
+
};
|
|
455
|
+
let hasError = errors.length > 0;
|
|
456
|
+
if(hasError)runLog.warn('[Task Error]', subTask.subid, errors);
|
|
457
|
+
let retried = false;
|
|
458
|
+
if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
459
|
+
let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
|
|
460
|
+
if(retryCount <= config.maxTaskRetries){
|
|
461
|
+
runLog.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
462
|
+
retried = true;
|
|
463
|
+
//等 rename 落盘再 resolve/请求新任务: fire-and-forget 的异步 rename 会随 worker 被
|
|
464
|
+
//autoClose 杀掉而消亡, 任务文件滞留 running 被静默丢弃(高负载偶发丢任务的根因, 勿回退)
|
|
465
|
+
try{
|
|
466
|
+
await TaskMgr.mvTaskPromise(subTask.subid, 'running', 'new');
|
|
467
|
+
}catch(newPath){
|
|
468
|
+
runLog.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
|
|
469
|
+
};
|
|
470
|
+
TaskMgr.deleteUserMessage(subTask.subid);//重跑即新一轮: 清掉该任务前序尝试的遗留消息
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
TaskMgr.saveTaskLog(subTask.subid, subTaskLog);
|
|
474
|
+
if(retried){
|
|
475
|
+
resolve({
|
|
476
|
+
startTimestamp,
|
|
477
|
+
endTimestamp,
|
|
478
|
+
cost,
|
|
479
|
+
retried: true
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
};
|
|
483
|
+
if(!hasError){
|
|
484
|
+
TaskMgr.deleteSavedResult(subTask.subid);//清掉前序尝试可能残留的(超时竞争)错误结果
|
|
485
|
+
};
|
|
486
|
+
TaskMgr.saveTaskResult(subTask.subid, hasError, hasError ? errors : resultData);
|
|
487
|
+
TaskMgr.mvTask(subTask.subid, 'running',(hasError ? 'finished_with_errors':'finished'), (newPath)=>{
|
|
488
|
+
resolve({
|
|
489
|
+
startTimestamp,
|
|
490
|
+
endTimestamp,
|
|
491
|
+
cost,
|
|
492
|
+
retried: false
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
};
|
|
496
498
|
module.exports = {start};
|