multi-tasks 3.1.6 → 3.1.8
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 +15 -4
- package/package.json +36 -36
- package/utils/foreachResults.js +30 -0
- package/workers/Multithread.js +82 -40
- package/workers/TaskMgr.js +24 -0
- package/workers/index.js +3 -6
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The API has three parts: the entry functions, the config options, and the `helpe
|
|
|
17
17
|
**Entry functions:**
|
|
18
18
|
|
|
19
19
|
- **`multiTasks(config)`** — run tasks; auto-resumes if the task folder already exists or `initialTasks` points to one.
|
|
20
|
-
- **`multiTasks.resume(config)`** — resume an interrupted run; pass a config object with `taskFolder` (the full task folder path). The whole config (including `processTask`) is restored from `task_config.json` in that folder; any field you pass overrides the snapshot, and overrides are written back into it.
|
|
20
|
+
- **`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.
|
|
21
21
|
- **`multiTasks.retry_fails(config)`** — same as `resume`, and also retries the failed tasks.
|
|
22
22
|
- **`multiTasks.restart(config)`** — wipe the task folder's progress and re-run from scratch; pass a config object with `taskFolder`. The config (including `processTask` and `initialTasks`) is restored from `task_config.json`; any field you pass overrides the snapshot, and overrides are written back into it. **All previous progress and results are deleted.**
|
|
23
23
|
|
|
@@ -33,11 +33,14 @@ The API has three parts: the entry functions, the config options, and the `helpe
|
|
|
33
33
|
- **`autoCloseAfterCompletion`** — set `true` to kill workers and exit the process when no tasks are left; default is `false`, i.e. workers stay alive waiting for dynamically created tasks.
|
|
34
34
|
- **`shouldTerminate(info)`** — return `true` to terminate the whole process.
|
|
35
35
|
- **`setSysListener(event, payload, meta)`** — optional, handle custom system events (sent via `helper.emitSys`) on the master; `meta` has `fromWorkerId` and `fromWorkerPid`.
|
|
36
|
-
- **`onFinish(report)`** — called once after all workers exit (only when `autoCloseAfterCompletion` is `true`)
|
|
36
|
+
- **`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:
|
|
37
|
+
- **`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).
|
|
38
|
+
- **`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', ...}`).
|
|
39
|
+
- 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.
|
|
37
40
|
|
|
38
41
|
**Other options:**
|
|
39
42
|
|
|
40
|
-
- **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`.
|
|
43
|
+
- **`taskFolder`** — full task folder path; only used by `resume`/`retry_fails`/`restart`.
|
|
41
44
|
|
|
42
45
|
**Task helper** (the `helper` object passed as the second argument of `processTask`):
|
|
43
46
|
|
|
@@ -84,8 +87,14 @@ multiTasks({
|
|
|
84
87
|
shouldTerminate:(info)=>{
|
|
85
88
|
//return true if you need to terminate the whole process
|
|
86
89
|
},
|
|
87
|
-
onFinish: (report)=>{
|
|
90
|
+
onFinish: (report, helper)=>{
|
|
88
91
|
console.log('finish callback', report);
|
|
92
|
+
helper.foreachSuccResult((count, task, result)=>{
|
|
93
|
+
console.log(`#${count} succeeded:`, task.name, result);
|
|
94
|
+
});
|
|
95
|
+
helper.foreachErrorResult((count, task, err)=>{
|
|
96
|
+
console.log(`#${count} failed:`, task.name, err);
|
|
97
|
+
});
|
|
89
98
|
}
|
|
90
99
|
});
|
|
91
100
|
|
|
@@ -263,6 +272,8 @@ Unlike `retry_fails` (which re-runs only the failed tasks), `restart` re-runs **
|
|
|
263
272
|
|
|
264
273
|
### Changelog:
|
|
265
274
|
|
|
275
|
+
- 3.1.8 Small refinements
|
|
276
|
+
- 3.1.7 onFinish: iterate all succ/failed results via helper.foreachSuccResult/foreachErrorResult
|
|
266
277
|
- 3.1.6 Fix silent task loss under heavy load with maxTaskRetries
|
|
267
278
|
- 3.1.5 Update README
|
|
268
279
|
- 3.1.4 Rewrite methods multiTasks.resume/retry_fails/restart
|
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "multi-tasks",
|
|
3
|
-
"version": "3.1.
|
|
4
|
-
"description": "Multi-process task scheduling based on Node.js cluster, with crash resume and failed-task restart support",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"index.js",
|
|
8
|
-
"workers",
|
|
9
|
-
"utils/*.js"
|
|
10
|
-
],
|
|
11
|
-
"repository": {
|
|
12
|
-
"type": "git",
|
|
13
|
-
"url": "https://gitee.com/zhanglei923/multi-tasks.git"
|
|
14
|
-
},
|
|
15
|
-
"directories": {
|
|
16
|
-
"example": "examples"
|
|
17
|
-
},
|
|
18
|
-
"engines": {
|
|
19
|
-
"node": ">=14.14"
|
|
20
|
-
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"test": "jest",
|
|
23
|
-
"prepublishOnly": "npm test"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"multitask",
|
|
27
|
-
"worker",
|
|
28
|
-
"multithread"
|
|
29
|
-
],
|
|
30
|
-
"author": "zhanglei923@gmail.com",
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"cli-progress": "^3.12.0",
|
|
34
|
-
"jest": "^29.7.0"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "multi-tasks",
|
|
3
|
+
"version": "3.1.8",
|
|
4
|
+
"description": "Multi-process task scheduling based on Node.js cluster, with crash resume and failed-task restart support",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"workers",
|
|
9
|
+
"utils/*.js"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://gitee.com/zhanglei923/multi-tasks.git"
|
|
14
|
+
},
|
|
15
|
+
"directories": {
|
|
16
|
+
"example": "examples"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=14.14"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"prepublishOnly": "npm test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"multitask",
|
|
27
|
+
"worker",
|
|
28
|
+
"multithread"
|
|
29
|
+
],
|
|
30
|
+
"author": "zhanglei923@gmail.com",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"cli-progress": "^3.12.0",
|
|
34
|
+
"jest": "^29.7.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
//同步遍历某个结果目录(results/succ 或 results/errors)下的全部 .json 结果文件,
|
|
5
|
+
//逐个回调 cb(count, task, result):
|
|
6
|
+
// count —— 0 起始的遍历序号;
|
|
7
|
+
// task —— 按 subid 从 taskProgressFolder 读回的任务文件内容(缺失/解析失败为 null);
|
|
8
|
+
// result —— 结果文件的 JSON 内容(解析失败, 如字面量 'undefined', 传 undefined, 不跳过)。
|
|
9
|
+
//仅在 onFinish 时机使用: 此时文件已全部落盘, 无并发写。
|
|
10
|
+
const forEachResult = (resultFolder, taskProgressFolder, cb) => {
|
|
11
|
+
const files = fs.readdirSync(resultFolder).filter((f) => f.endsWith('.json'));
|
|
12
|
+
files.forEach((fname, count) => {
|
|
13
|
+
const subid = fname.replace(/\.json$/, '');
|
|
14
|
+
let task = null;
|
|
15
|
+
try {
|
|
16
|
+
task = JSON.parse(fs.readFileSync(path.join(taskProgressFolder, `${subid}.task.json`), 'utf-8'));
|
|
17
|
+
} catch (e) {
|
|
18
|
+
task = null;
|
|
19
|
+
};
|
|
20
|
+
let result;
|
|
21
|
+
try {
|
|
22
|
+
result = JSON.parse(fs.readFileSync(path.join(resultFolder, fname), 'utf-8'));
|
|
23
|
+
} catch (e) {
|
|
24
|
+
result = undefined;
|
|
25
|
+
};
|
|
26
|
+
cb(count, task, result);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = { forEachResult };
|
package/workers/Multithread.js
CHANGED
|
@@ -58,7 +58,10 @@ const start = (config)=>{
|
|
|
58
58
|
console.log(` PID=${PID}`);
|
|
59
59
|
|
|
60
60
|
const AsyncQueue = require(`../utils/asyncQueue`);
|
|
61
|
-
const queue = new AsyncQueue();
|
|
61
|
+
const queue = new AsyncQueue();
|
|
62
|
+
|
|
63
|
+
const masterStartTimestamp = new Date()*1;//本轮 master 启动时间(resume 不追溯首轮)
|
|
64
|
+
let workerCrashCount = 0;//累计崩溃补员次数, 进 onFinish report
|
|
62
65
|
|
|
63
66
|
let clearTaskTimer = (workerId)=>{
|
|
64
67
|
let rec = workerTaskTimers[workerId];
|
|
@@ -155,49 +158,64 @@ const start = (config)=>{
|
|
|
155
158
|
}
|
|
156
159
|
if(message === MSG_REQUEST_NEW_TASK){
|
|
157
160
|
clearTaskTimer(worker.id);//上一个任务已结束(无论成败), 撤销超时倒计时
|
|
158
|
-
queue.enqueue(async ()=>{
|
|
159
|
-
return TaskMgr.updateNewTasks();
|
|
160
|
-
});//use queue to make sure no I/O conflict
|
|
161
161
|
queue.enqueue(async () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
subTask
|
|
186
|
-
},()=>{}, {
|
|
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,()=>{}, {
|
|
187
185
|
keepOpen: (typeof config.keepOpen === 'undefined') ? false : config.keepOpen
|
|
188
186
|
});
|
|
189
|
-
let hasTimeout = typeof config.taskTimeout === 'number' && config.taskTimeout > 0;
|
|
190
|
-
let hasRetry = typeof config.maxTaskRetries === 'number' && config.maxTaskRetries > 0;
|
|
191
|
-
if(hasTimeout || hasRetry){
|
|
192
|
-
workerTaskTimers[worker.id] = {
|
|
193
|
-
timer: hasTimeout ? setTimeout(()=>{ onTaskTimeout(worker); }, config.taskTimeout) : null,
|
|
194
|
-
subTask,
|
|
195
|
-
startTimestamp
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
taskCount++;
|
|
199
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
|
|
200
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++;
|
|
201
219
|
});
|
|
202
220
|
}
|
|
203
221
|
});
|
|
@@ -211,12 +229,18 @@ const start = (config)=>{
|
|
|
211
229
|
let worker = cluster.fork();
|
|
212
230
|
initWorker(worker);
|
|
213
231
|
}
|
|
232
|
+
}).catch((e)=>{
|
|
233
|
+
//init/resume 异步失败(rename/读盘错误等): 明确 FATAL + 非 0 退出,
|
|
234
|
+
//否则 unhandled rejection 在 warn 模式下会不 fork 不退出、静默挂死
|
|
235
|
+
console.error('[FATAL] failed to init tasks:', e);
|
|
236
|
+
process.exit(1);
|
|
214
237
|
});
|
|
215
238
|
cluster.on('exit', (worker, code, signal) => {
|
|
216
239
|
let rec = workerTaskTimers[worker.id];//先取再清: 崩溃重试要用 rec.subTask
|
|
217
240
|
clearTaskTimer(worker.id);//worker 退出(含崩溃)时清掉其超时计时器
|
|
218
241
|
if (code !== 0 && !worker.exitedAfterDisconnect) {
|
|
219
242
|
console.log('Worker died unexpectedly starting a new one...');
|
|
243
|
+
workerCrashCount++;
|
|
220
244
|
if(rec && rec.subTask){
|
|
221
245
|
handleCrashedTask(rec.subTask);
|
|
222
246
|
};
|
|
@@ -230,7 +254,25 @@ const start = (config)=>{
|
|
|
230
254
|
}
|
|
231
255
|
if(allkilled){
|
|
232
256
|
if(USER_CONFIG.onFinish){
|
|
233
|
-
|
|
257
|
+
let endTimestamp = new Date()*1;
|
|
258
|
+
let stats = TaskMgr.getFinalStats();
|
|
259
|
+
let report = {
|
|
260
|
+
taskId: config.masterTaskId,
|
|
261
|
+
taskFolder: stats.taskFolder,
|
|
262
|
+
startTimestamp: masterStartTimestamp,
|
|
263
|
+
endTimestamp,
|
|
264
|
+
cost: endTimestamp - masterStartTimestamp,
|
|
265
|
+
total: stats.succeeded + stats.failed,
|
|
266
|
+
succeeded: stats.succeeded,
|
|
267
|
+
failed: stats.failed,
|
|
268
|
+
failedTasks: stats.failedTasks,
|
|
269
|
+
workers: {count: numOfWorkers, crashed: workerCrashCount},
|
|
270
|
+
};
|
|
271
|
+
let onFinishHelper = {
|
|
272
|
+
foreachSuccResult: (cb) => TaskMgr.forEachSuccResult(cb),
|
|
273
|
+
foreachErrorResult: (cb) => TaskMgr.forEachErrorResult(cb),
|
|
274
|
+
};
|
|
275
|
+
USER_CONFIG.onFinish(report, onFinishHelper);
|
|
234
276
|
delete USER_CONFIG.onFinish;
|
|
235
277
|
}
|
|
236
278
|
setTimeout(()=>{
|
package/workers/TaskMgr.js
CHANGED
|
@@ -3,6 +3,7 @@ const pathutil = require('path');
|
|
|
3
3
|
const makeDir = require('../utils/makedir');
|
|
4
4
|
const randomsUtil = require('../utils/randoms');
|
|
5
5
|
const { serializeConfig } = require('../utils/serializeFns');
|
|
6
|
+
const { forEachResult } = require('../utils/foreachResults');
|
|
6
7
|
|
|
7
8
|
let allSubTasks = {
|
|
8
9
|
new:[],
|
|
@@ -175,6 +176,21 @@ const TaskMgr = {
|
|
|
175
176
|
callback(files);
|
|
176
177
|
});
|
|
177
178
|
},
|
|
179
|
+
getFinalStats:()=>{//master 收尾用: 以盘上终态为准统计成功/失败(resume 跨轮累计也是对的)
|
|
180
|
+
const {taskFolder, taskFolderFinished, taskFolderFailed} = main_config;
|
|
181
|
+
const listSubids = (folder)=>{
|
|
182
|
+
return fs.readdirSync(folder)
|
|
183
|
+
.filter((fname)=>fname.endsWith('.task.json'))
|
|
184
|
+
.map((fname)=>fname.replace(/\.task\.json$/, ''));
|
|
185
|
+
};
|
|
186
|
+
const failedTasks = listSubids(taskFolderFailed);
|
|
187
|
+
return {
|
|
188
|
+
taskFolder,
|
|
189
|
+
succeeded: listSubids(taskFolderFinished).length,
|
|
190
|
+
failed: failedTasks.length,
|
|
191
|
+
failedTasks,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
178
194
|
mvTaskPromise: (subid, from, to)=> {
|
|
179
195
|
return new Promise((resolve, reject)=>{
|
|
180
196
|
TaskMgr.mvTask(subid, from, to, (newPath, succ)=>{
|
|
@@ -257,6 +273,14 @@ const TaskMgr = {
|
|
|
257
273
|
delete task.__retryCount;
|
|
258
274
|
fs.writeFileSync(fpath, JSON.stringify(task));
|
|
259
275
|
},
|
|
276
|
+
forEachSuccResult:(cb)=>{
|
|
277
|
+
let {taskFolderResult, taskFolderFinished} = main_config;
|
|
278
|
+
forEachResult(taskFolderResult, taskFolderFinished, cb);
|
|
279
|
+
},
|
|
280
|
+
forEachErrorResult:(cb)=>{
|
|
281
|
+
let {taskFolderResultFailed, taskFolderFailed} = main_config;
|
|
282
|
+
forEachResult(taskFolderResultFailed, taskFolderFailed, cb);
|
|
283
|
+
},
|
|
260
284
|
resetTaskFolders:(taskFolder)=>{
|
|
261
285
|
fs.rmSync(taskFolder, {recursive: true, force: true});
|
|
262
286
|
},
|
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);
|