multi-tasks 3.1.9 â 3.2.1
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 +58 -63
- package/package.json +1 -1
- package/utils/progressCtl.js +118 -0
- package/utils/progressView.js +35 -0
- package/utils/runLog.js +136 -0
- package/utils/startupBanner.js +50 -0
- package/workers/Multithread.js +78 -38
- package/workers/TaskMgr.js +10 -4
- package/workers/WorkerMgr.js +3 -1
- package/workers/asMaster.js +0 -3
- package/workers/helper.js +8 -2
- package/workers/index.js +13 -0
package/README.md
CHANGED
|
@@ -10,69 +10,7 @@ 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
|
-
|
|
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
|
-
|
|
38
|
-
### API:
|
|
39
|
-
|
|
40
|
-
The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
|
|
41
|
-
|
|
42
|
-
**Entry functions:**
|
|
43
|
-
|
|
44
|
-
- **`multiTasks(config)`** â run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
|
|
45
|
-
- **`multiTasks.resume(config)`** â usually you don't need this: the simplest way to resume is to just run `multiTasks(config)` again, it auto-resumes when the task folder already exists. This method resumes an interrupted run from the task folder alone; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
|
|
46
|
-
- **`multiTasks.retry_fails(config)`** â same as `resume`, and also retries the failed tasks.
|
|
47
|
-
- **`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.**
|
|
48
|
-
|
|
49
|
-
**Config options:**
|
|
50
|
-
|
|
51
|
-
- **`initialTasks`** â array of task objects, or a folder path string to resume from.
|
|
52
|
-
- **`processTask(task, helper)`** â required; return a value or a Promise.
|
|
53
|
-
- **`taskRootFolder`** â the task run folder (named by `taskId`) is created under this directory.
|
|
54
|
-
- **`taskId`** â task folder name under `taskRootFolder`.
|
|
55
|
-
- **`numberOfWorkers`** â how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
|
|
56
|
-
- **`taskTimeout`** â optional, milliseconds; an overdue task fails with a timeout error.
|
|
57
|
-
- **`maxTaskRetries`** â optional, max times a failed task is auto-retried.
|
|
58
|
-
- **`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.
|
|
59
|
-
- **`shouldTerminate(info)`** â return `true` to terminate the whole process.
|
|
60
|
-
- **`setSysListener(event, payload, meta)`** â optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
|
|
61
|
-
- **`onFinish(report, helper)`** â called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, workers }` â `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `workers` is `{ count, crashed }` â the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
|
|
62
|
-
- **`helper.foreachSuccResult((count, task, result) => {})`** â iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing).
|
|
63
|
-
- **`helper.foreachErrorResult((count, task, err) => {})`** â iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`).
|
|
64
|
-
- Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns â don't `await` inside it.
|
|
65
|
-
|
|
66
|
-
**Other options:**
|
|
67
|
-
|
|
68
|
-
- **`taskFolder`** â full task folder path; only used by `resume`/`retry_fails`/`restart`.
|
|
69
|
-
|
|
70
|
-
**Task helper** (the `helper` object passed as the second argument of `processTask`):
|
|
71
|
-
|
|
72
|
-
- **`helper.createNewTasks(tasks)`** â create new tasks dynamically while processing.
|
|
73
|
-
- **`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.
|
|
74
|
-
- **`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.
|
|
75
|
-
- **`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`.
|
|
13
|
+
Tip: monitor running tasks in real time â see [Real-Time Monitoring](#real-time-monitoring).
|
|
76
14
|
|
|
77
15
|
### How to use:
|
|
78
16
|
|
|
@@ -295,8 +233,65 @@ multiTasks.restart({ taskFolder: `/myworks/my_scan_tasks/my-task` }); //deletes
|
|
|
295
233
|
|
|
296
234
|
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.)
|
|
297
235
|
|
|
236
|
+
### Real-Time Monitoring
|
|
237
|
+
|
|
238
|
+
Monitor your running tasks with [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) â see its npm page for full details.
|
|
239
|
+
|
|
240
|
+
Quick start (requires multi-tasks >=3.2.0): once your multi-tasks run has started, the startup log prints the monitor command with the task folder path already filled in â just copy-paste it into another terminal:
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
****************************************************************
|
|
244
|
+
[Master]: monitor this task with:
|
|
245
|
+
npx multi-tasks-monitor --task-dir "D:\tasks\multitasks20260806..." --port 3777
|
|
246
|
+
****************************************************************
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
For installation, command-line options, and more, see the [multi-tasks-monitor](https://www.npmjs.com/package/multi-tasks-monitor) page.
|
|
250
|
+
|
|
251
|
+
### API:
|
|
252
|
+
|
|
253
|
+
The API has three parts: the entry functions, the config options, and the `helper` object injected into `processTask`.
|
|
254
|
+
|
|
255
|
+
**Entry functions:**
|
|
256
|
+
|
|
257
|
+
- **`multiTasks(config)`** â run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
|
|
258
|
+
- **`multiTasks.resume(config)`** â usually you don't need this: the simplest way to resume is to just run `multiTasks(config)` again, it auto-resumes when the task folder already exists. This method resumes an interrupted run from the task folder alone; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
|
|
259
|
+
- **`multiTasks.retry_fails(config)`** â same as `resume`, and also retries the failed tasks.
|
|
260
|
+
- **`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.**
|
|
261
|
+
|
|
262
|
+
**Config options:**
|
|
263
|
+
|
|
264
|
+
- **`initialTasks`** â array of task objects, or a folder path string to resume from.
|
|
265
|
+
- **`processTask(task, helper)`** â required; return a value or a Promise.
|
|
266
|
+
- **`taskRootFolder`** â the task run folder (named by `taskId`) is created under this directory.
|
|
267
|
+
- **`taskId`** â task folder name under `taskRootFolder`.
|
|
268
|
+
- **`numberOfWorkers`** â how many worker processes run in parallel; a number, or a percentage string of CPU cores like `"50%"` (default).
|
|
269
|
+
- **`taskTimeout`** â optional, milliseconds; an overdue task fails with a timeout error.
|
|
270
|
+
- **`maxTaskRetries`** â optional, max times a failed task is auto-retried.
|
|
271
|
+
- **`progressBar`** â `true` by default: shows a live single-line progress bar in the terminal (set `false` to disable); logs (including your own `console.*` calls) always go to `<taskFolder>/.sys/run.log`, and print to the console as well only when the bar is off or stdout is not a TTY.
|
|
272
|
+
- **`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.
|
|
273
|
+
- **`shouldTerminate(info)`** â return `true` to terminate the whole process.
|
|
274
|
+
- **`setSysListener(event, payload, meta)`** â optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
|
|
275
|
+
- **`onFinish(report, helper)`** â called once after all workers exit (only when `autoCloseAfterCompletion` is `true`). `report` summarizes the run: `{ taskId, taskFolder, startTimestamp, endTimestamp, cost, total, succeeded, failed, failedTasks, workers }` â `cost` is the run duration in ms (a resumed run measures only the current round); `failedTasks` lists the failed tasks' subids; `workers` is `{ count, crashed }` â the configured worker count and how many workers crashed and were replaced. `helper` lets you walk every persisted task result:
|
|
276
|
+
- **`helper.foreachSuccResult((count, task, result) => {})`** â iterate all succeeded results (`results/succ`); `count` is a 0-based index, `task` is the original task object (`null` if its task file is missing or unparseable), `result` is the value returned by `processTask` (`undefined` when the task returned nothing).
|
|
277
|
+
- **`helper.foreachErrorResult((count, task, err) => {})`** â iterate all failed results (`results/errors`); `err` is the persisted error object as-is (`{ex}` / `{subTask, exception}` / `{type: 'timeout', ...}` / `{type: 'worker_crash', ...}`).
|
|
278
|
+
- Iteration follows filesystem order (not sorted). `onFinish` runs synchronously at the very end and the master process exits shortly after it returns â don't `await` inside it.
|
|
279
|
+
|
|
280
|
+
**Other options:**
|
|
281
|
+
|
|
282
|
+
- **`taskFolder`** â full task folder path; only used by `resume`/`retry_fails`/`restart`.
|
|
283
|
+
|
|
284
|
+
**Task helper** (the `helper` object passed as the second argument of `processTask`):
|
|
285
|
+
|
|
286
|
+
- **`helper.createNewTasks(tasks)`** â create new tasks dynamically while processing.
|
|
287
|
+
- **`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.
|
|
288
|
+
- **`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.
|
|
289
|
+
- **`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`.
|
|
290
|
+
|
|
298
291
|
### Changelog:
|
|
299
292
|
|
|
293
|
+
- 3.2.1 Fix retry bug: user-defined taskCount/index no longer overwritten on task retries
|
|
294
|
+
- 3.2.0 Add progressBar (on by default); console output diverted to run.log; new startup banner
|
|
300
295
|
- 3.1.9 Print the multi-tasks-monitor command on startup
|
|
301
296
|
- 3.1.8 Small refinements
|
|
302
297
|
- 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//progressBar master į̝æ§åļå¨(éļäžčĩ): æææ¸˛æå¨åŽäžä¸åŋįĸ襨, å°čŖ
TTY å¤åŽä¸čæĩæ¸˛æ,
|
|
2
|
+
//Multithread åĒč°č¯äšåæšæŗ(setup/workerStarted/taskAssigned/...), ä¸įĸ°æ¸˛æįģčã
|
|
3
|
+
//įē¯ master į̝čŋčĄæļįļæ, ä¸čŋåŋĢį
§ä¸čŋ IPC; įģčŽĄæ°æŽ(getStats/getRemaining)įąč°į¨æšæŗ¨å
Ĩ, æŦæ¨Ąåä¸äžčĩ TaskMgrã
|
|
4
|
+
//console åĢæä¸ run.log åˇ˛ä¸ progressBar č§ŖčĻ(æ°¸äš
įæ, č§ utils/runLog), ä¸åæ¯æŦæ¨Ąåįčč´Ŗã
|
|
5
|
+
//åčĄčŋåēĻæĄ(progressView): worker åĸå ä¸äŊį°å¨æ¸˛æä¸, čŋéåĒįģ´æ¤ busyWorkers čŽĄæ° runningã
|
|
6
|
+
const progressView = require('./progressView');
|
|
7
|
+
|
|
8
|
+
let RENDER_INTERVAL = 100;//æ¸˛æčæĩ: éĢéĸå°äģģåĄå翝éŋå
æ¯æŦĄäēäģļéŊ readdirSync(getFinalStats) é æ O(N²) įæĢ
|
|
9
|
+
|
|
10
|
+
let view = null;//progressView æ¸˛æå¨åŽäž, æĒåŧå¯ä¸ē null
|
|
11
|
+
let progressOn = false;//progressBar ä¸ stdout ä¸ē TTY æļä¸ēį(é TTY åéæŽéæĨåŋ)
|
|
12
|
+
let busyWorkers = {};//worker.id -> subid, master į̝åŋįĸ襨
|
|
13
|
+
let getStats = null;//() => TaskMgr.getFinalStats(), setup æŗ¨å
Ĩ
|
|
14
|
+
let getRemaining = null;//() => new įŽåŊäģģåĄæ°, setup æŗ¨å
Ĩ
|
|
15
|
+
let lastRenderAt = 0;
|
|
16
|
+
let renderTimer = null;
|
|
17
|
+
|
|
18
|
+
const reset = () => {//master start å
ĨåŖ/åčŋį¨åčˇä¸čŊŽæļč°į¨: æ¸
æä¸ä¸čŊŽå
¨é¨æŽį(åĢæĒåįčæĩ帧)
|
|
19
|
+
if (renderTimer) {
|
|
20
|
+
clearTimeout(renderTimer);
|
|
21
|
+
renderTimer = null;
|
|
22
|
+
};
|
|
23
|
+
view = null;
|
|
24
|
+
progressOn = false;
|
|
25
|
+
busyWorkers = {};
|
|
26
|
+
lastRenderAt = 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//åŧå¯å¤åŽ + åå§å: čŋ忝åĻįæŖåŧå¯(æžåŧ progressBar:false å
ŗé, įŧēįéģ莤åŧå¯;
|
|
30
|
+
//é TTY ä¸åžåéæŽéæĨåŋ, äģ
æžåŧ true æļæåéčĻå, éģ莤åŧå¯ééģåé)
|
|
31
|
+
const setup = ({ config, stream, getStats: statsFn, getRemaining: remainingFn }) => {
|
|
32
|
+
reset();
|
|
33
|
+
if (config.progressBar === false) {
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
if (!stream.isTTY) {//ANSI æ§åļį åčŋéåŽåčžå翝ååž: åéä¸ēį°ææ§åļå°æĨåŋ
|
|
37
|
+
if (config.progressBar === true) {
|
|
38
|
+
console.warn('[Master]: progressBar needs a TTY stdout, fallback to console logs');
|
|
39
|
+
};
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
42
|
+
progressOn = true;
|
|
43
|
+
getStats = statsFn;
|
|
44
|
+
getRemaining = remainingFn;
|
|
45
|
+
view = progressView.create({ stream, barWidth: 30 });
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isOn = () => {
|
|
50
|
+
return progressOn;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const doRender = () => {
|
|
54
|
+
//åŽææ°æįä¸įģæįŽåŊįģ莥: éč¯/čļ
æļ/å´Šæēåį§čˇ¯åžå¤ŠįļæŖįĄŽ, æ éå¨å莎ééĸå¤čŽĄæ°
|
|
55
|
+
let stats = getStats();
|
|
56
|
+
let done = stats.succeeded + stats.failed;
|
|
57
|
+
let running = Object.keys(busyWorkers).length;
|
|
58
|
+
lastRenderAt = Date.now();
|
|
59
|
+
view.render({ done, running, remaining: getRemaining() });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const render = () => {
|
|
63
|
+
if (!view) {
|
|
64
|
+
return;
|
|
65
|
+
};
|
|
66
|
+
if (renderTimer) {
|
|
67
|
+
return;//厞æįä¸ä¸Ēå°žéæ¸˛æ, ååšļæä¸é´å¸§
|
|
68
|
+
};
|
|
69
|
+
let wait = RENDER_INTERVAL - (Date.now() - lastRenderAt);
|
|
70
|
+
if (wait <= 0) {
|
|
71
|
+
doRender();
|
|
72
|
+
return;
|
|
73
|
+
};
|
|
74
|
+
renderTimer = setTimeout(() => {
|
|
75
|
+
renderTimer = null;
|
|
76
|
+
if (view) {
|
|
77
|
+
doRender();
|
|
78
|
+
};
|
|
79
|
+
}, wait);
|
|
80
|
+
renderTimer.unref();//ä¸éģįĸ master éåē(æåä¸å¸§å¯čŊå čŋį¨éåēä¸ĸå¤ą, 坿Ĩå)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const workerStarted = (id, pid) => {//åčĄčŋåēĻæĄä¸åąį¤ē worker čĄ: äģ
äŋįč¯äšå
ĨåŖ, æ æ¸˛æå¨äŊ
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const taskAssigned = (id, subid) => {
|
|
87
|
+
busyWorkers[id] = subid;
|
|
88
|
+
render();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const taskCompleted = (id) => {
|
|
92
|
+
if (busyWorkers[id]) {
|
|
93
|
+
delete busyWorkers[id];
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const workerExited = (id) => {
|
|
98
|
+
delete busyWorkers[id];
|
|
99
|
+
render();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const finish = () => {//æļå°ž: æĸčĄåŽæ ŧčŋåēĻæĄčĄ(master éåēåč°į¨); æĒåŧå¯ä¸ē no-op
|
|
103
|
+
if (view) {
|
|
104
|
+
view.end();
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
setup,
|
|
110
|
+
reset,
|
|
111
|
+
isOn,
|
|
112
|
+
render,
|
|
113
|
+
workerStarted,
|
|
114
|
+
taskAssigned,
|
|
115
|
+
taskCompleted,
|
|
116
|
+
workerExited,
|
|
117
|
+
finish,
|
|
118
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//įģį̝čŋåēĻæĄæ¸˛æå¨(éļäžčĩ): åčĄčŋåēĻæĄ, æ¯å¸§ \r åčĄéĻ + æ¸
čĄ + éå, ä¸ä¸į§ģå
æ ã䏿ĸčĄã
|
|
2
|
+
//į睿¸˛æ, ä¸įĸ° cluster/fsãåčĄåŊĸæå¯š"两帧äšé´æå¤é¨åå
Ĩå
ąäēĢįģį̝"鞿Ŗ:
|
|
3
|
+
//æ§å¸§æå¤įä¸čĄæģ卿Žį, ä¸ä¸å¸§å¨åŊåčĄčĒæ(å¤čĄįé ESC[nA æ°čĄ, å¤é¨åå
Ĩæ¤åå
æ äŧ莊æ§å¸§æ°¸äš
æģį)ã
|
|
4
|
+
const ESC = '';
|
|
5
|
+
|
|
6
|
+
const create = ({ stream, barWidth }) => {
|
|
7
|
+
if (!stream) { throw new Error('progressView needs a stream'); };
|
|
8
|
+
const width = barWidth || 30;
|
|
9
|
+
|
|
10
|
+
const buildBarLine = ({ done, running, remaining }) => {
|
|
11
|
+
let total = done + running + remaining;
|
|
12
|
+
let filled = total > 0 ? Math.floor(width * done / total) : width;
|
|
13
|
+
let bar = 'â'.repeat(filled) + 'â'.repeat(width - filled);
|
|
14
|
+
let pct = (total > 0 ? 100 * done / total : 100).toFixed(2).padStart(6);//两äŊå°æ°, åŽåŽŊé˛čĄéŋæå¨
|
|
15
|
+
return `[${bar}] ${pct}% ${done}/${total} done, ${running} running`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const render = ({ done, running, remaining }) => {
|
|
19
|
+
//čĄæ¯įģį̝ååŽŊčŋéŋäŧæčĄã\r åä¸å°å¸§éĻ: æååŽŊæĒæ
|
|
20
|
+
let cols = stream.columns || 80;
|
|
21
|
+
let line = buildBarLine({ done, running, remaining });
|
|
22
|
+
if (line.length > cols) {
|
|
23
|
+
line = line.slice(0, cols);
|
|
24
|
+
};
|
|
25
|
+
stream.write(`\r${ESC}[2K${line}`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const end = () => {//æļå°žæĸčĄ: čŋåēĻæĄčĄåŽæ ŧ, åįģčžåē(shell æį¤ēįŦĻį)čŊå¨ä¸ä¸čĄ
|
|
29
|
+
stream.write('\n');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { render, end };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = { create };
|
package/utils/runLog.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//æĨåŋåæĩ(æ°¸äš
æ§æēåļ, ä¸ progressBar æ å
ŗ): åēįčŋčĄæĨåŋä¸ hook åį console.* ä¸åžįģ O_APPEND
|
|
2
|
+
//åæĩ(åŧæĨ, ä¸éģåĄäēäģļåžĒį¯) append å°äģģåĄįŽåŊį run.log; echo æ¨Ąåŧ(progressBar æĒåŧ/é TTY åé)
|
|
3
|
+
//åæļéäŧ å console ä¸åą, é echo æ¨Ąåŧ(progressBar čŋåēĻåēæĨįŽĄįģį̝)åĒåæäģļä¸ä¸åąã
|
|
4
|
+
//master ä¸ worker åčŋį¨åæä¸ä¸Ēåæĩ append å䏿äģļã
|
|
5
|
+
//best-effort: åæĩåēé(įŽåŊä¸åå¨/æéį)æ čŽ°åæ´čŊŽä¸ĸåŧ; čŋį¨éåēå¯čŊä¸ĸæĒ flush įįŧå˛å°žé¨ã
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const util = require('util');
|
|
8
|
+
|
|
9
|
+
let state = { enabled: false, filePath: null, stream: null, broken: false, hooked: false, echo: false };
|
|
10
|
+
let originalConsole = null;
|
|
11
|
+
|
|
12
|
+
const init = (filePath) => {
|
|
13
|
+
state.enabled = true;
|
|
14
|
+
state.filePath = filePath;
|
|
15
|
+
state.stream = null;//æ°æ§ååģē: éĻä¸Ē write æļæ open, éŋå
įŠēčˇäšåģēæäģļ
|
|
16
|
+
state.broken = false;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const reset = () => {//åčŋį¨åčˇä¸čŊŽ multiTasks æļå¤äŊ, åĻåæĨåŋäŧįģ§įģåä¸ä¸čŊŽį run.log
|
|
20
|
+
unhookConsole();
|
|
21
|
+
if (state.stream) {
|
|
22
|
+
state.stream.destroy();
|
|
23
|
+
};
|
|
24
|
+
state.enabled = false;
|
|
25
|
+
state.filePath = null;
|
|
26
|
+
state.stream = null;
|
|
27
|
+
state.broken = false;
|
|
28
|
+
state.echo = false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isEnabled = () => {
|
|
32
|
+
return state.enabled;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
//æĻæĒ console: æ console.log/warn/error æŋæĸä¸ēå run.log įåŽį°,
|
|
36
|
+
//፿ˇäģŖį (processTask/onFinish į)éį console čžåēåæ ˇčŋæäģļã
|
|
37
|
+
//echo ä¸ēįæļåæļč°ååŽį°ä¸åą(progressBar æĒåŧ/é TTY åé); ä¸ēåæļåĒåæäģļ(čŋåēĻåēæĨįŽĄįģį̝)ã
|
|
38
|
+
//reset/unhookConsole æĸå¤ååŽį°(åŋ
éĄģå¨ enabled å false äšåæĸå¤, åĻå write éäŧ console äŧéåŊ)ã
|
|
39
|
+
const hookConsole = ({ echo } = {}) => {
|
|
40
|
+
if (!state.enabled || state.hooked) {
|
|
41
|
+
return;
|
|
42
|
+
};
|
|
43
|
+
state.hooked = true;
|
|
44
|
+
state.echo = !!echo;
|
|
45
|
+
originalConsole = { log: console.log, warn: console.warn, error: console.error };
|
|
46
|
+
console.log = write;
|
|
47
|
+
console.warn = warn;
|
|
48
|
+
console.error = error;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const unhookConsole = () => {
|
|
52
|
+
if (!state.hooked) {
|
|
53
|
+
return;
|
|
54
|
+
};
|
|
55
|
+
console.log = originalConsole.log;
|
|
56
|
+
console.warn = originalConsole.warn;
|
|
57
|
+
console.error = originalConsole.error;
|
|
58
|
+
originalConsole = null;
|
|
59
|
+
state.hooked = false;
|
|
60
|
+
state.echo = false;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formatArg = (arg) => {
|
|
64
|
+
if (arg instanceof Error) {
|
|
65
|
+
return arg.stack || String(arg);
|
|
66
|
+
};
|
|
67
|
+
if (typeof arg === 'string') {
|
|
68
|
+
return arg;
|
|
69
|
+
};
|
|
70
|
+
//å¯ščąĄ/æ°įģčĩ° util.inspect, éŋå
åčŋ run.log åæ [object Object] ä¸ĸäŋĄæ¯
|
|
71
|
+
return typeof arg === 'object' && arg !== null ? util.inspect(arg) : String(arg);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const appendToFile = (args) => {
|
|
75
|
+
if (state.broken) {
|
|
76
|
+
return;
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
if (!state.stream) {
|
|
80
|
+
state.stream = fs.createWriteStream(state.filePath, { flags: 'a' });
|
|
81
|
+
state.stream.on('error', () => {//åįå¤ąč´Ĩä¸åŊąåäģģåĄčŋčĄ, æ čŽ°åæ´čŊŽä¸ĸåŧ(éŋå
åžĒį¯éåģē)
|
|
82
|
+
state.broken = true;
|
|
83
|
+
state.stream.destroy();
|
|
84
|
+
state.stream = null;
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
state.stream.write(args.map(formatArg).join(' ') + '\n');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
//best-effort: æĨåŋåįå¤ąč´Ĩä¸åŊąåäģģåĄčŋčĄ
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const write = (...args) => {
|
|
94
|
+
if (!state.enabled) {
|
|
95
|
+
console.log(...args);
|
|
96
|
+
return;
|
|
97
|
+
};
|
|
98
|
+
appendToFile(args);
|
|
99
|
+
if (state.echo && originalConsole) {
|
|
100
|
+
originalConsole.log(...args);
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const warn = (...args) => {
|
|
105
|
+
if (!state.enabled) {
|
|
106
|
+
console.warn(...args);
|
|
107
|
+
return;
|
|
108
|
+
};
|
|
109
|
+
appendToFile(args);
|
|
110
|
+
if (state.echo && originalConsole) {
|
|
111
|
+
originalConsole.warn(...args);
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const error = (...args) => {
|
|
116
|
+
if (!state.enabled) {
|
|
117
|
+
console.error(...args);
|
|
118
|
+
return;
|
|
119
|
+
};
|
|
120
|
+
appendToFile(args);
|
|
121
|
+
if (state.echo && originalConsole) {
|
|
122
|
+
originalConsole.error(...args);
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
//master/worker į̝å
ĨåŖč°į¨: æåæĩ莝åžåŗ init åšļ hook console(æ°¸äš
æ§æēåļ, ä¸åå¤ TTY/progressBar);
|
|
127
|
+
//echo įąč°į¨æšæ progressBar æ¯åĻæĨįŽĄįģį̝åŗåŽãæ čˇ¯åžæļäģäšéŊä¸å(įģ´æ console)ã夿æļæå¨æ¤, č°į¨įšä¸å ifã
|
|
128
|
+
const initAndHook = (filePath, { echo } = {}) => {
|
|
129
|
+
if (!filePath) {
|
|
130
|
+
return;
|
|
131
|
+
};
|
|
132
|
+
init(filePath);
|
|
133
|
+
hookConsole({ echo });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
module.exports = { init, initAndHook, reset, isEnabled, write, warn, error, hookConsole, unhookConsole };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//å¯å¨äŋĄæ¯æ¨Ēåš
æ¸˛æå¨(įē¯åŊæ°, ä¸įĸ° console/fs/cluster): äģ
æ éĸčĄ ASCII æĄčŖ
éĨ°,
|
|
2
|
+
//æĄåŽŊåĒįąæ éĸčĄå
厚åŗåŽââéŋ莝åžåĒåēį°å¨åšŗéēåæŽĩčĄ, ä¸äŧæį ´æĄãä¸äŧæčĄã
|
|
3
|
+
//renderBanner: įģį̝ä¸åąį¨(åĢæå console į´åē); renderLogHeader: run.log æäģļ头é¨į¨(åĢ MODE čĄ);
|
|
4
|
+
//writeLogHeader: å¤ sysLogFile åå runLogââ夿æļæå¨æšæŗå
é¨, č°į¨įšä¸å if;
|
|
5
|
+
//æ čˇ¯åžæļ(å
é¨ API multiTasks.start čĒå¨įæįŽåŊå)no-op, ä¸éäŧ consoleã
|
|
6
|
+
const runLog = require('./runLog');
|
|
7
|
+
|
|
8
|
+
const VERSION = require('../package.json').version;
|
|
9
|
+
|
|
10
|
+
const KEY_WIDTH = 17;//éŽååŽŊ: é厚įēŗæéŋéŽ 'monitor start cmd', æšå¨æļåæĨč°
|
|
11
|
+
|
|
12
|
+
const renderTitleBox = (mode) => {
|
|
13
|
+
const left = `multi-tasks v${VERSION}`;
|
|
14
|
+
const right = `MODE: ${mode}`;
|
|
15
|
+
const innerWidth = left.length + right.length + 4;//åˇĻåŗå
厚é´čŗå° 4 įŠēæ ŧ
|
|
16
|
+
const middle = `| ${left}${' '.repeat(innerWidth - left.length - right.length)}${right} |`;
|
|
17
|
+
const border = `+${'-'.repeat(innerWidth + 2)}+`;
|
|
18
|
+
return [border, middle, border];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const renderFieldLines = (info) => {
|
|
22
|
+
const rows = [
|
|
23
|
+
['taskId', info.taskId],
|
|
24
|
+
['taskFolder', info.taskFolder],
|
|
25
|
+
['workers', `${info.numOfWorkers} (numCPUs=${info.numCPUs})`],
|
|
26
|
+
['master PID', `${info.pid}`],
|
|
27
|
+
['new tasks', `${info.newTasks}`],
|
|
28
|
+
['run.log', info.sysLogFile || '(not available)'],
|
|
29
|
+
['started at', info.startedAt],
|
|
30
|
+
['monitor start cmd', `npx multi-tasks-monitor --task-dir "${info.taskFolder}" --port 3777`],
|
|
31
|
+
];
|
|
32
|
+
return rows.map(([key, value]) => `${key.padEnd(KEY_WIDTH)} : ${value}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const renderBanner = (info) => {
|
|
36
|
+
return [...renderTitleBox(info.mode), ...renderFieldLines(info)].join('\n');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const renderLogHeader = (info) => {
|
|
40
|
+
return [`${'MODE'.padEnd(KEY_WIDTH)} : ${info.mode}`, ...renderFieldLines(info)].join('\n');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const writeLogHeader = (info) => {
|
|
44
|
+
if (!info.sysLogFile) {
|
|
45
|
+
return;
|
|
46
|
+
};
|
|
47
|
+
runLog.write(renderLogHeader(info));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = { renderBanner, renderLogHeader, writeLogHeader };
|
package/workers/Multithread.js
CHANGED
|
@@ -9,6 +9,9 @@ const TaskMgr = require('./TaskMgr');
|
|
|
9
9
|
const WorkerMgr = require('./WorkerMgr');
|
|
10
10
|
const helper = require('./helper');
|
|
11
11
|
const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
|
|
12
|
+
const runLog = require('../utils/runLog');
|
|
13
|
+
const progressCtl = require('../utils/progressCtl');
|
|
14
|
+
const startupBanner = require('../utils/startupBanner');
|
|
12
15
|
|
|
13
16
|
let fmtdigit = (n)=>{
|
|
14
17
|
return n > 9 ? "" + n: "0" + n;
|
|
@@ -34,7 +37,7 @@ let killAllWorkers = ()=>{
|
|
|
34
37
|
for (const id in cluster.workers) {
|
|
35
38
|
if (cluster.workers.hasOwnProperty(id)) {
|
|
36
39
|
const worker = cluster.workers[id];
|
|
37
|
-
|
|
40
|
+
runLog.write(`Killing worker ${worker.process.pid}`);
|
|
38
41
|
killWorker(worker);
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -49,13 +52,12 @@ const start = (config)=>{
|
|
|
49
52
|
let isMaster = (cluster.isPrimary || cluster.isMaster);
|
|
50
53
|
|
|
51
54
|
if (isMaster) {//init
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
//åčŋį¨åčˇä¸čŊŽ multiTasks æļå¤äŊ progressBar æ§åļå¨įļæ,
|
|
56
|
+
//åĻåæŽįäŧ莊æ°ä¸čŊŽ(ä¸å¸Ļ progressBar)įģ§į쿏˛ææ§čŋåēĻåē
|
|
57
|
+
progressCtl.reset();
|
|
58
|
+
runLog.reset();//runLog 厞čąįĻģ progressCtl: ä¸å¤äŊåæ°ä¸čŊŽæĨåŋįģ§įģåæ§ run.logãconsole äģčĸĢæ§ hook æĻæĒ
|
|
54
59
|
config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//æ¯æįžåæ¯åįŦĻ串; éĄģå¨ fork åžĒį¯č¯ģåŧåæįŽ, éæŗåŧ卿¤æé
|
|
55
60
|
let numOfWorkers = config.numberOfWorkers;
|
|
56
|
-
console.log(` numCPUs=${numCPUs}`);
|
|
57
|
-
console.log(` numOfWorkers=${numOfWorkers}`);
|
|
58
|
-
console.log(` PID=${PID}`);
|
|
59
61
|
|
|
60
62
|
const AsyncQueue = require(`../utils/asyncQueue`);
|
|
61
63
|
const queue = new AsyncQueue();
|
|
@@ -82,7 +84,7 @@ const start = (config)=>{
|
|
|
82
84
|
if(typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
83
85
|
let retryCount = TaskMgr.incrementTaskRetry(subid);
|
|
84
86
|
if(retryCount <= config.maxTaskRetries){
|
|
85
|
-
|
|
87
|
+
runLog.warn(`[Master]: task "${subid}" timeout, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
86
88
|
TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
|
|
87
89
|
return;
|
|
88
90
|
};
|
|
@@ -91,7 +93,7 @@ const start = (config)=>{
|
|
|
91
93
|
TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
|
|
92
94
|
}
|
|
93
95
|
}catch(e){
|
|
94
|
-
|
|
96
|
+
runLog.warn('[Master]: failed to finalize timeout task', subid, e);
|
|
95
97
|
}
|
|
96
98
|
};
|
|
97
99
|
let onTaskTimeout = (worker)=>{
|
|
@@ -100,7 +102,7 @@ const start = (config)=>{
|
|
|
100
102
|
delete workerTaskTimers[worker.id];
|
|
101
103
|
let {subTask, startTimestamp} = rec;
|
|
102
104
|
let cost = new Date()*1 - startTimestamp;
|
|
103
|
-
|
|
105
|
+
runLog.warn(`[Master]: task "${subTask.subid}" timeout after ${cost}ms, killing worker pid=${worker.process.pid}`);
|
|
104
106
|
worker.kill();//ä¸ disconnect: exit äēäģļäŧæå´ŠæēčĄĨå
|
|
105
107
|
finalizeTimeoutTask(subTask, cost);
|
|
106
108
|
};
|
|
@@ -110,15 +112,15 @@ const start = (config)=>{
|
|
|
110
112
|
try{
|
|
111
113
|
let retryCount = TaskMgr.incrementTaskRetry(subid);
|
|
112
114
|
if(retryCount <= config.maxTaskRetries){
|
|
113
|
-
|
|
115
|
+
runLog.warn(`[Master]: worker crashed, retry task "${subid}" (${retryCount}/${config.maxTaskRetries})`);
|
|
114
116
|
TaskMgr.mvTask(subid, 'running', 'new', ()=>{});
|
|
115
117
|
}else{
|
|
116
|
-
|
|
118
|
+
runLog.warn(`[Master]: task "${subid}" crashed ${retryCount} times, give up`);
|
|
117
119
|
TaskMgr.saveTaskResult(subid, true, [{type: 'worker_crash', retryCount: config.maxTaskRetries}]);
|
|
118
120
|
TaskMgr.mvTask(subid, 'running', 'finished_with_errors', ()=>{});
|
|
119
121
|
};
|
|
120
122
|
}catch(e){
|
|
121
|
-
|
|
123
|
+
runLog.warn('[Master]: failed to handle crashed task', subid, e);
|
|
122
124
|
}
|
|
123
125
|
};
|
|
124
126
|
|
|
@@ -127,8 +129,9 @@ const start = (config)=>{
|
|
|
127
129
|
worker.on('message', function(message) {
|
|
128
130
|
if(message && message.__sysEvent){//worker įŗģįģäēäģļ: master čĒåˇąæļč´š, ä¸čŊŦå
|
|
129
131
|
if(message.event === 'TERMINATE_ALL_WORKERS'){
|
|
130
|
-
|
|
132
|
+
runLog.warn(`[Master]: TERMINATE_ALL_WORKERS from worker ${worker.id}, killing all workers`);
|
|
131
133
|
killAllWorkers();
|
|
134
|
+
progressCtl.finish();//åčĄčŋåēĻæĄæĸčĄåŽæ ŧ, åĢ莊 shell æį¤ēįŦĻįäŊåŽ
|
|
132
135
|
return process.exit(0);
|
|
133
136
|
}
|
|
134
137
|
let meta = {fromWorkerId: worker.id, fromWorkerPid: worker.process.pid};
|
|
@@ -136,10 +139,10 @@ const start = (config)=>{
|
|
|
136
139
|
try{
|
|
137
140
|
USER_CONFIG.setSysListener(message.event, message.payload, meta);
|
|
138
141
|
}catch(e){
|
|
139
|
-
|
|
142
|
+
runLog.error(`[Master]: setSysListener for event "${message.event}" threw:`, e);
|
|
140
143
|
}
|
|
141
144
|
}else{
|
|
142
|
-
|
|
145
|
+
runLog.warn(`[Master]: unknown sys event "${message.event}" from worker ${worker.id} (no setSysListener configured)`);
|
|
143
146
|
}
|
|
144
147
|
return;
|
|
145
148
|
}
|
|
@@ -151,13 +154,14 @@ const start = (config)=>{
|
|
|
151
154
|
try{
|
|
152
155
|
w.send({__broadcast: true, event: message.event, payload: message.payload, meta});
|
|
153
156
|
}catch(e){
|
|
154
|
-
|
|
157
|
+
runLog.warn(`[Master]: failed to relay broadcast to worker ${w.id}`, e);
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
return;
|
|
158
161
|
}
|
|
159
162
|
if(message === MSG_REQUEST_NEW_TASK){
|
|
160
163
|
clearTaskTimer(worker.id);//ä¸ä¸ä¸ĒäģģåĄåˇ˛įģæ(æ čŽēæč´Ĩ), æ¤éčļ
æļå莥æļ
|
|
164
|
+
progressCtl.taskCompleted(worker.id);//worker čĄåå° idle; åŽææ°æįä¸įģæįŽåŊįģ莥, ä¸å¨æ¤čŽĄæ°
|
|
161
165
|
queue.enqueue(async () => {
|
|
162
166
|
//use queue to make sure no I/O conflict; update/pop/å¤įŠēéĄģå¨åä¸ä¸Ē串čĄäģģåĄéåååŽæ
|
|
163
167
|
await TaskMgr.updateNewTasks();
|
|
@@ -177,7 +181,7 @@ const start = (config)=>{
|
|
|
177
181
|
}
|
|
178
182
|
if(!subTask){
|
|
179
183
|
//no tasks left
|
|
180
|
-
|
|
184
|
+
runLog.write('[Master]: no tasks left!');
|
|
181
185
|
if(config.autoCloseAfterCompletion){
|
|
182
186
|
killWorker(worker);
|
|
183
187
|
}else{//waiting for new tasks
|
|
@@ -185,6 +189,7 @@ const start = (config)=>{
|
|
|
185
189
|
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
186
190
|
});
|
|
187
191
|
}
|
|
192
|
+
progressCtl.render();
|
|
188
193
|
return;
|
|
189
194
|
}
|
|
190
195
|
let terminateData = {taskCount,subTask,config, id: worker.id};
|
|
@@ -206,6 +211,7 @@ const start = (config)=>{
|
|
|
206
211
|
},()=>{}, {
|
|
207
212
|
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
208
213
|
});
|
|
214
|
+
progressCtl.taskAssigned(worker.id, subTask.subid);
|
|
209
215
|
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
210
216
|
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
211
217
|
if(hasTimeout || hasRetry){
|
|
@@ -223,17 +229,37 @@ const start = (config)=>{
|
|
|
223
229
|
|
|
224
230
|
let initPromise = config.__resume ? asMaster.resume(config) : asMaster.init(config);
|
|
225
231
|
initPromise.then((updated_config)=>{
|
|
226
|
-
console.log(` new tasks=${TaskMgr.getTasks('new').length}`);
|
|
227
232
|
config = updated_config;
|
|
228
|
-
console
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
//䏿ŦĄæ§å¯å¨å(čĩ° console, å¨åĢæåä¸åą): æ éĸæĄæ¨Ēåš
+ åšŗéēå
ŗéŽåæŽĩ, æ¨Ąåŧįą __ æ 莰æ¨å¯ŧ;
|
|
234
|
+
//init æååææäŋĄæ¯éŊå¤(new tasks æ°/masterTaskId/__sysLogFile), 䏿ŦĄæåŽ
|
|
235
|
+
let startupInfo = {
|
|
236
|
+
mode: config.__restart ? 'RESTART' : (config.__retryFails ? 'RETRY_FAILS' : (config.__resume ? 'RESUME' : 'START')),
|
|
237
|
+
taskId: config.masterTaskId,
|
|
238
|
+
taskFolder: config.taskFolder,
|
|
239
|
+
numOfWorkers,
|
|
240
|
+
numCPUs,
|
|
241
|
+
pid: process.pid,
|
|
242
|
+
newTasks: TaskMgr.getTasks('new').length,
|
|
243
|
+
sysLogFile: config.__sysLogFile,
|
|
244
|
+
startedAt: getDateTimeTxt(),
|
|
245
|
+
};
|
|
246
|
+
console.log(startupBanner.renderBanner(startupInfo));
|
|
247
|
+
progressCtl.setup({//TTY å¤åŽ + æ¸˛æå¨ååģēéŊ卿§åļå¨å
; įģ莥åŖåžæŗ¨å
Ĩ, æ§åļå¨ä¸äžčĩ TaskMgr
|
|
248
|
+
config,
|
|
249
|
+
stream: process.stdout,
|
|
250
|
+
getStats: () => TaskMgr.getFinalStats(),
|
|
251
|
+
getRemaining: () => TaskMgr.getTasks('new').length,
|
|
252
|
+
});
|
|
253
|
+
//console åĢæå run.log æ¯æ°¸äš
æēåļ, ä¸ progressBar æ å
ŗ: čŋåēĻåēæĨįŽĄįģį̝(åŧä¸ TTY)æļåĒåæäģļ,
|
|
254
|
+
//åĻå echo åæļéäŧ console ä¸åą; å¤åŽæļæå¨ initAndHook å
, æ __sysLogFile æļįģ´æ console
|
|
255
|
+
runLog.initAndHook(config.__sysLogFile, { echo: !progressCtl.isOn() });
|
|
256
|
+
startupBanner.writeLogHeader(startupInfo);//åä¸įģå
ŗéŽäŋĄæ¯å run.log 头é¨; æ __sysLogFile æļå
é¨ no-op
|
|
233
257
|
for (let i = 0; i < numOfWorkers; i++) {
|
|
234
258
|
let worker = cluster.fork();
|
|
235
259
|
initWorker(worker);
|
|
260
|
+
progressCtl.workerStarted(worker.id, worker.process.pid);
|
|
236
261
|
}
|
|
262
|
+
progressCtl.render();
|
|
237
263
|
}).catch((e)=>{
|
|
238
264
|
//init/resume åŧæĨå¤ąč´Ĩ(rename/č¯ģįé蝝į): æįĄŽ FATAL + é 0 éåē,
|
|
239
265
|
//åĻå unhandled rejection å¨ warn æ¨Ąåŧä¸äŧä¸ fork ä¸éåēãééģææģ
|
|
@@ -243,14 +269,17 @@ const start = (config)=>{
|
|
|
243
269
|
cluster.on('exit', (worker, code, signal) => {
|
|
244
270
|
let rec = workerTaskTimers[worker.id];//å
å忏
: å´Šæēéč¯čĻ፠rec.subTask
|
|
245
271
|
clearTaskTimer(worker.id);//worker éåē(åĢå´Šæē)æļæ¸
æå
ļčļ
æļ莥æļå¨
|
|
272
|
+
progressCtl.workerExited(worker.id);
|
|
246
273
|
if (code !== 0 && !worker.exitedAfterDisconnect) {
|
|
247
|
-
|
|
274
|
+
runLog.write('Worker died unexpectedly starting a new one...');
|
|
248
275
|
workerCrashCount++;
|
|
249
276
|
if(rec && rec.subTask){
|
|
250
277
|
handleCrashedTask(rec.subTask);
|
|
251
278
|
};
|
|
252
279
|
let worker = cluster.fork();
|
|
253
280
|
initWorker(worker);
|
|
281
|
+
progressCtl.workerStarted(worker.id, worker.process.pid);
|
|
282
|
+
progressCtl.render();
|
|
254
283
|
return;
|
|
255
284
|
}
|
|
256
285
|
let allkilled = true;
|
|
@@ -281,24 +310,30 @@ const start = (config)=>{
|
|
|
281
310
|
delete USER_CONFIG.onFinish;
|
|
282
311
|
}
|
|
283
312
|
setTimeout(()=>{
|
|
284
|
-
|
|
313
|
+
runLog.write('[Master]: all workers are done, process exit.')
|
|
314
|
+
progressCtl.finish();//åčĄčŋåēĻæĄæĸčĄåŽæ ŧ, åĢ莊 shell æį¤ēįŦĻįäŊåŽ
|
|
285
315
|
process.exit(0);
|
|
286
316
|
}, 10);
|
|
287
317
|
}
|
|
288
318
|
});
|
|
289
319
|
}else{
|
|
290
|
-
subWorker();
|
|
320
|
+
subWorker(config);
|
|
291
321
|
}
|
|
292
322
|
};
|
|
293
|
-
function subWorker(){
|
|
323
|
+
function subWorker(config){
|
|
294
324
|
let worker = cluster.worker;
|
|
325
|
+
//worker čŋį¨įŦįĢéčˇå
ĨåŖ, config.__sysLogFile åˇ˛įą workers/index.js å
ĨåŖįŽåĨŊ(ä¸ master åä¸čˇ¯åž):
|
|
326
|
+
//console åĢæå run.log æ°¸äš
įæ, ä¸ progressBar æ å
ŗ; progressBar æĒæžåŧå
ŗé(įŧēįéģ莤åŧå¯)ä¸
|
|
327
|
+
//å
ąäēĢ stdout ä¸ē TTY æļåĒåæäģļ(ä¸ master å¤åŽä¸č´), åĻå echo åæļä¸åąãå¤åŽæļæå¨ initAndHook å
,
|
|
328
|
+
//æ čˇ¯åžæļįģ´æ consoleãå
ĨåŖåŗįæ, éĸä¸å°äģģåĄįå¯äŊ worker į waiting æĨåŋåæ ˇčŋ run.logã
|
|
329
|
+
runLog.initAndHook(config.__sysLogFile, { echo: !(config.progressBar !== false && process.stdout.isTTY) });
|
|
295
330
|
worker.on('message', function(message) {
|
|
296
331
|
if(message && message.__broadcast){//master ä¸čŊŦæĨįåšŋæ: ååįģ helper.on æŗ¨åį handler
|
|
297
332
|
helper.__dispatchBroadcast(message);
|
|
298
333
|
return;
|
|
299
334
|
}
|
|
300
335
|
if(message === MSG_NO_TASK_FOUND){//wait for new task
|
|
301
|
-
|
|
336
|
+
runLog.write(` (Worker #${worker.id}): waiting`);
|
|
302
337
|
setTimeout(()=>{
|
|
303
338
|
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
304
339
|
}, 5000 + Math.random()*5000);
|
|
@@ -306,11 +341,13 @@ function subWorker(){
|
|
|
306
341
|
if(message.subTask){//new task
|
|
307
342
|
let {config, subTask, startTimestamp, taskCount} = message;
|
|
308
343
|
TaskMgr.load(config);
|
|
309
|
-
|
|
310
|
-
|
|
344
|
+
//፿ˇäģģåĄčĒå¸Ļį index/taskCount äŋįååŧ(éč¯/įģčˇåäģģåĄčēĢäģŊä¸å),
|
|
345
|
+
//æĒæäžæļæåĄĢå
¨åąæ´žå莥æ°å¨(v1 å
ŧ厚)
|
|
346
|
+
if(typeof subTask.index === 'undefined') subTask.index = taskCount;
|
|
347
|
+
if(typeof subTask.taskCount === 'undefined') subTask.taskCount = taskCount;
|
|
311
348
|
subTask.workerId = worker.id;
|
|
312
349
|
subTask.workerPid = process.pid;
|
|
313
|
-
|
|
350
|
+
runLog.write(`[Task Start]: "${subTask.subid}"`, `pid=${process.pid}`, getDateTimeTxt());
|
|
314
351
|
let subTaskLog = {
|
|
315
352
|
workerId: worker.id,
|
|
316
353
|
workerPid: process.pid,
|
|
@@ -318,14 +355,15 @@ function subWorker(){
|
|
|
318
355
|
startTimestamp
|
|
319
356
|
};
|
|
320
357
|
runTask(config, subTask, subTaskLog).then((tasklog)=>{
|
|
358
|
+
if(tasklog.retried) return;//éč¯äģģåĄåˇ˛å new įéčˇ, æĒåŽæ, 䏿 [Task Finished]
|
|
321
359
|
let {cost, startTimestamp, endTimestamp} = tasklog;
|
|
322
360
|
|
|
323
|
-
|
|
361
|
+
runLog.write(`[Task Finished] "${config.masterTaskId}"."${subTask.subid}".`, getDateTimeTxt(), `cost=${cost}ms`)
|
|
324
362
|
}).then(()=>{
|
|
325
363
|
worker.send(MSG_REQUEST_NEW_TASK);//request new task
|
|
326
364
|
});
|
|
327
365
|
}else{
|
|
328
|
-
|
|
366
|
+
runLog.write('Can not find: message.subTask');
|
|
329
367
|
throw new Error('can not find subTask!')
|
|
330
368
|
}
|
|
331
369
|
});
|
|
@@ -366,19 +404,19 @@ function runTask(config, subTask, subTaskLog){
|
|
|
366
404
|
cost
|
|
367
405
|
};
|
|
368
406
|
let hasError = errors.length > 0;
|
|
369
|
-
if(hasError)
|
|
407
|
+
if(hasError)runLog.warn('[Task Error]', subTask.subid, errors);
|
|
370
408
|
let retried = false;
|
|
371
409
|
if(hasError && typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0){
|
|
372
410
|
let retryCount = TaskMgr.incrementTaskRetry(subTask.subid);
|
|
373
411
|
if(retryCount <= config.maxTaskRetries){
|
|
374
|
-
|
|
412
|
+
runLog.warn(`[Task Retry] "${subTask.subid}" failed, retry (${retryCount}/${config.maxTaskRetries})`);
|
|
375
413
|
retried = true;
|
|
376
414
|
//į rename čŊįå resolve/č¯ˇæąæ°äģģåĄ: fire-and-forget įåŧæĨ rename äŧé worker čĸĢ
|
|
377
415
|
//autoClose ææčæļäēĄ, äģģåĄæäģļæģį running čĸĢééģä¸ĸåŧ(éĢč´čŊŊåļåä¸ĸäģģåĄįæ šå , åŋåé)
|
|
378
416
|
try{
|
|
379
417
|
await TaskMgr.mvTaskPromise(subTask.subid, 'running', 'new');
|
|
380
418
|
}catch(newPath){
|
|
381
|
-
|
|
419
|
+
runLog.warn(`[Task Retry] "${subTask.subid}" failed to move back to new, left in running (recoverable via resume)`);
|
|
382
420
|
};
|
|
383
421
|
};
|
|
384
422
|
};
|
|
@@ -387,7 +425,8 @@ function runTask(config, subTask, subTaskLog){
|
|
|
387
425
|
resolve({
|
|
388
426
|
startTimestamp,
|
|
389
427
|
endTimestamp,
|
|
390
|
-
cost
|
|
428
|
+
cost,
|
|
429
|
+
retried: true
|
|
391
430
|
});
|
|
392
431
|
return;
|
|
393
432
|
};
|
|
@@ -399,7 +438,8 @@ function runTask(config, subTask, subTaskLog){
|
|
|
399
438
|
resolve({
|
|
400
439
|
startTimestamp,
|
|
401
440
|
endTimestamp,
|
|
402
|
-
cost
|
|
441
|
+
cost,
|
|
442
|
+
retried: false
|
|
403
443
|
});
|
|
404
444
|
});
|
|
405
445
|
});
|
package/workers/TaskMgr.js
CHANGED
|
@@ -4,6 +4,7 @@ const makeDir = require('../utils/makedir');
|
|
|
4
4
|
const randomsUtil = require('../utils/randoms');
|
|
5
5
|
const { serializeConfig } = require('../utils/serializeFns');
|
|
6
6
|
const { forEachResult } = require('../utils/foreachResults');
|
|
7
|
+
const runLog = require('../utils/runLog');
|
|
7
8
|
|
|
8
9
|
let allSubTasks = {
|
|
9
10
|
new:[],
|
|
@@ -35,6 +36,7 @@ const TaskMgr = {
|
|
|
35
36
|
const progressRootFolder = pathutil.resolve(taskFolder, './progress');
|
|
36
37
|
const resultsRootFolder = pathutil.resolve(taskFolder, './results');
|
|
37
38
|
const logsRootFolder = pathutil.resolve(taskFolder, './logs');
|
|
39
|
+
const sysRootFolder = pathutil.resolve(taskFolder, './.sys');//åēčĒčēĢčŋčĄæĨåŋ(run.log)įįŗģįģæäģļ, ä¸äģģåĄæ°æŽ/įģæéįĻģ
|
|
38
40
|
|
|
39
41
|
const taskFolderNew = pathutil.resolve(progressRootFolder, './new');
|
|
40
42
|
const taskFolderRunning = pathutil.resolve(progressRootFolder, './running');
|
|
@@ -44,6 +46,7 @@ const TaskMgr = {
|
|
|
44
46
|
const taskFolderResult = pathutil.resolve(resultsRootFolder, './succ');
|
|
45
47
|
const taskFolderResultFailed = pathutil.resolve(resultsRootFolder, './errors');
|
|
46
48
|
makeDir.sync(logsRootFolder);
|
|
49
|
+
makeDir.sync(sysRootFolder);
|
|
47
50
|
makeDir.sync(taskFolderNew);
|
|
48
51
|
makeDir.sync(taskFolderRunning);
|
|
49
52
|
makeDir.sync(taskFolderFinished);
|
|
@@ -57,6 +60,9 @@ const TaskMgr = {
|
|
|
57
60
|
progressRootFolder,
|
|
58
61
|
resultsRootFolder,
|
|
59
62
|
logsRootFolder,
|
|
63
|
+
sysRootFolder,
|
|
64
|
+
//run.log 莝åžå¯ä¸å
åē莥įŽįš: å
ĨåŖ(workers/index.js)æ˛ĄįŽčŋæļå¨čŋéčĄĨ(__ åįŧ, ä¸čŋåŋĢį
§)
|
|
65
|
+
__sysLogFile: main_config.__sysLogFile || pathutil.resolve(sysRootFolder, 'run.log'),
|
|
60
66
|
|
|
61
67
|
taskFolderNew,
|
|
62
68
|
taskFolderRunning,
|
|
@@ -97,14 +103,14 @@ const TaskMgr = {
|
|
|
97
103
|
try{
|
|
98
104
|
fcontent = fs.readFileSync(fpath, 'utf-8');
|
|
99
105
|
}catch(e){
|
|
100
|
-
|
|
106
|
+
runLog.warn('[WARN]', `file not found`, fpath)
|
|
101
107
|
}
|
|
102
108
|
if(fcontent){
|
|
103
109
|
try{
|
|
104
110
|
let task = JSON.parse(fcontent);
|
|
105
111
|
allSubTasks[type].push(task);
|
|
106
112
|
}catch(e){
|
|
107
|
-
|
|
113
|
+
runLog.warn('[WARN]', 'failed to parse task file, skipped', fpath);
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
});
|
|
@@ -199,7 +205,7 @@ const TaskMgr = {
|
|
|
199
205
|
});
|
|
200
206
|
},
|
|
201
207
|
mvTask: (subid, from, to, callback)=> {
|
|
202
|
-
|
|
208
|
+
runLog.write('Move task:', subid, `${from}>${to}`);
|
|
203
209
|
let {progressRootFolder} = main_config;
|
|
204
210
|
let oldPath = pathutil.resolve(progressRootFolder, `./${from}/${subid}.task.json`);
|
|
205
211
|
let newPath = pathutil.resolve(progressRootFolder, `./${to}/${subid}.task.json`);
|
|
@@ -212,7 +218,7 @@ const TaskMgr = {
|
|
|
212
218
|
}
|
|
213
219
|
});
|
|
214
220
|
}catch(e){
|
|
215
|
-
|
|
221
|
+
runLog.warn(e);
|
|
216
222
|
callback(newPath, false);
|
|
217
223
|
}
|
|
218
224
|
},
|
package/workers/WorkerMgr.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
let workers = {};
|
|
2
2
|
let status = {};
|
|
3
3
|
|
|
4
|
+
const runLog = require('../utils/runLog');
|
|
5
|
+
|
|
4
6
|
const mgr = {
|
|
5
7
|
addWorker:(worker)=>{
|
|
6
8
|
let id = `${worker.id}`;
|
|
@@ -8,7 +10,7 @@ const mgr = {
|
|
|
8
10
|
throw new Error('FATAL: duplicated worker!');
|
|
9
11
|
}
|
|
10
12
|
status[id] = {isBusy: true};
|
|
11
|
-
|
|
13
|
+
runLog.write('add worker', worker.id);
|
|
12
14
|
workers[id] = worker;
|
|
13
15
|
},
|
|
14
16
|
getWorker:(id)=>{
|
package/workers/asMaster.js
CHANGED
|
@@ -77,8 +77,6 @@ const init = (config)=>{
|
|
|
77
77
|
let randomNum = round((random() * pow(10,8)),8).toString(36);
|
|
78
78
|
let masterTaskId = config.taskId ? config.taskId : `${taskName}${dateTimeText}--${randomNum}`;
|
|
79
79
|
|
|
80
|
-
console.log(` masterTaskId===${masterTaskId}`);
|
|
81
|
-
|
|
82
80
|
config.taskRootFolder = taskRootFolder;
|
|
83
81
|
config.masterTaskId = masterTaskId;
|
|
84
82
|
|
|
@@ -93,7 +91,6 @@ const init = (config)=>{
|
|
|
93
91
|
//to reload is for to reStart a collapsed task
|
|
94
92
|
return new Promise((resolve)=>{
|
|
95
93
|
TaskMgr.loadNewTasks().then(()=>{
|
|
96
|
-
console.log('******start multi-thread')
|
|
97
94
|
resolve(config);
|
|
98
95
|
});
|
|
99
96
|
});
|
package/workers/helper.js
CHANGED
|
@@ -2,6 +2,7 @@ let fs = require('fs');
|
|
|
2
2
|
let pathutil = require('path');
|
|
3
3
|
|
|
4
4
|
let TaskMgr = require('./TaskMgr');
|
|
5
|
+
let runLog = require('../utils/runLog');
|
|
5
6
|
|
|
6
7
|
let myconfig;
|
|
7
8
|
let listeners = {};//event -> handler, worker čŋį¨å
æ¨Ąåįē§åšŋæįåŦ registry(æ¯ä¸Ē event ä¸ä¸Ē handler)
|
|
@@ -14,7 +15,7 @@ const createNewTasks = (tasks)=>{
|
|
|
14
15
|
if (!Array.isArray(tasks)) tasks = [tasks];
|
|
15
16
|
TaskMgr.load(myconfig);
|
|
16
17
|
tasks.forEach((task)=>{
|
|
17
|
-
|
|
18
|
+
runLog.write('Creating new task', task)
|
|
18
19
|
TaskMgr.createNewTask(task);
|
|
19
20
|
});
|
|
20
21
|
};
|
|
@@ -40,6 +41,10 @@ const setListener = (event, handler)=>{
|
|
|
40
41
|
if (typeof handler !== 'function') throw new Error('helper.setListener: handler must be a function');
|
|
41
42
|
listeners[event] = handler;
|
|
42
43
|
};
|
|
44
|
+
//äģģåĄæĨåŋ: ä¸åžå <taskFolder>/.sys/run.log; progressBar æĒåŧ/é TTY åéæļ echo ä¸åą(ææįå console.log)
|
|
45
|
+
const log = (...args)=>{
|
|
46
|
+
runLog.write(...args);
|
|
47
|
+
};
|
|
43
48
|
//å
鍿šæŗ: äž subWorker æļå° master ä¸čŊŦįåšŋææļč°į¨, æ event åå
|
|
44
49
|
const __dispatchBroadcast = (message)=>{
|
|
45
50
|
let {event, payload, meta} = message;
|
|
@@ -48,7 +53,7 @@ const __dispatchBroadcast = (message)=>{
|
|
|
48
53
|
try{
|
|
49
54
|
handler(payload, meta);
|
|
50
55
|
}catch(e){
|
|
51
|
-
|
|
56
|
+
runLog.error(`[Broadcast] handler for event "${event}" threw:`, e);
|
|
52
57
|
}
|
|
53
58
|
};
|
|
54
59
|
module.exports = {
|
|
@@ -57,5 +62,6 @@ module.exports = {
|
|
|
57
62
|
emit,
|
|
58
63
|
emitSys,
|
|
59
64
|
setListener,
|
|
65
|
+
log,
|
|
60
66
|
__dispatchBroadcast
|
|
61
67
|
};
|
package/workers/index.js
CHANGED
|
@@ -43,11 +43,21 @@ const _mergeSnapshotConfig = (config)=>{
|
|
|
43
43
|
return merged;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
//run.log 莝åžå¨å
ĨåŖįģä¸įŽåĨŊæå° config(__ åįŧ: 䏿ŦĄæ§æ´žįåŧ, ä¸čŋåŋĢį
§), ä¸ progressBar æ å
ŗ;
|
|
47
|
+
//master/worker åčŋį¨éŊéčˇå
ĨåŖãįŽåēåä¸čˇ¯åž, Multithread į´æĨäŊŋį¨, ä¸åäģģäŊčˇ¯åžæ¨å¯ŧã
|
|
48
|
+
const _computeSysLogFile = (config)=>{
|
|
49
|
+
let taskFolder = config.taskFolder || ((config.taskId && config.taskRootFolder) ? pathUtil.resolve(config.taskRootFolder, config.taskId) : null);
|
|
50
|
+
if(taskFolder){
|
|
51
|
+
config.__sysLogFile = pathUtil.resolve(taskFolder, '.sys', 'run.log');
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
46
55
|
const start = (config)=>{
|
|
47
56
|
config = backwardv1.v1tov2(config);
|
|
48
57
|
|
|
49
58
|
config.taskRootFolder = _toAbsPath(config.taskRootFolder);
|
|
50
59
|
config.taskFolder = _toAbsPath(config.taskFolder);
|
|
60
|
+
_computeSysLogFile(config);
|
|
51
61
|
|
|
52
62
|
Multithread.start(config);
|
|
53
63
|
};
|
|
@@ -65,6 +75,7 @@ const resume = (config)=>{
|
|
|
65
75
|
};
|
|
66
76
|
config.taskRootFolder = pathUtil.dirname(config.taskFolder);
|
|
67
77
|
config.taskId = pathUtil.basename(config.taskFolder);
|
|
78
|
+
_computeSysLogFile(config);
|
|
68
79
|
|
|
69
80
|
config = _mergeSnapshotConfig(config);
|
|
70
81
|
Multithread.start(config);
|
|
@@ -80,6 +91,7 @@ const retry_fails = (config)=>{
|
|
|
80
91
|
config.taskId = pathUtil.basename(taskFolder);
|
|
81
92
|
config.__resume = true;
|
|
82
93
|
config.__retryFails = true;
|
|
94
|
+
_computeSysLogFile(config);
|
|
83
95
|
config = _mergeSnapshotConfig(config);
|
|
84
96
|
Multithread.start(config);
|
|
85
97
|
};
|
|
@@ -93,6 +105,7 @@ const restart = (config)=>{
|
|
|
93
105
|
config.taskRootFolder = pathUtil.dirname(taskFolder);
|
|
94
106
|
config.taskId = pathUtil.basename(taskFolder);
|
|
95
107
|
config.__restart = true;
|
|
108
|
+
_computeSysLogFile(config);
|
|
96
109
|
config = _mergeSnapshotConfig(config);
|
|
97
110
|
Multithread.start(config);
|
|
98
111
|
};
|