multi-tasks 3.5.1 → 4.0.0-rc.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 +324 -326
- package/bench-storage.svg +15 -11
- package/package.json +1 -1
- package/utils/makedir.js +5 -4
- package/utils/progressCtl.js +7 -0
- package/utils/runLog.js +7 -2
- package/utils/startupBanner.js +5 -0
- package/workers/AofStore.js +349 -0
- package/workers/MemoryStore.js +337 -247
- package/workers/Multithread.js +92 -53
- package/workers/TaskMgr.js +52 -7
- package/workers/asMaster.js +138 -109
- package/workers/helper.js +77 -77
- package/workers/index.js +282 -224
- package/workers/resultFileAppender.js +62 -0
package/workers/index.js
CHANGED
|
@@ -1,224 +1,282 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const pathUtil = require('path');
|
|
3
|
-
const cluster = require('cluster');
|
|
4
|
-
const Multithread = require('./Multithread');
|
|
5
|
-
const backwardv1 = require('../utils/backward-v1');
|
|
6
|
-
const { FUNCTION_CONFIG_KEYS, reviveConfig } = require('../utils/serializeFns');
|
|
7
|
-
|
|
8
|
-
const TASK_STORAGE_MODES = ['file', 'memory'];
|
|
9
|
-
//task_storage
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if(
|
|
16
|
-
|
|
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
|
-
if(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if(
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const pathUtil = require('path');
|
|
3
|
+
const cluster = require('cluster');
|
|
4
|
+
const Multithread = require('./Multithread');
|
|
5
|
+
const backwardv1 = require('../utils/backward-v1');
|
|
6
|
+
const { FUNCTION_CONFIG_KEYS, reviveConfig } = require('../utils/serializeFns');
|
|
7
|
+
|
|
8
|
+
const TASK_STORAGE_MODES = ['file', 'memory', 'aof'];
|
|
9
|
+
//task_storage 三档: file(任务状态实时写磁盘, 可 resume) / memory(纯内存, 进程退出即丢) /
|
|
10
|
+
//aof(master 内存 + append-only aof, 可 resume, 接近 memory 档速度)。
|
|
11
|
+
//4.0.0 起缺省值 file -> aof; 新默认值只作用于 fresh start, resume 链路永远跟随目录里的真实模式(见 _resolveResumeTaskStorage)
|
|
12
|
+
const DEFAULT_TASK_STORAGE = 'aof';
|
|
13
|
+
const _resolveTaskStorage = (config)=>{
|
|
14
|
+
let mode = config.task_storage;
|
|
15
|
+
if(typeof mode === 'undefined' || mode === null){
|
|
16
|
+
mode = DEFAULT_TASK_STORAGE;
|
|
17
|
+
//一次性标记: 启动横幅据此提示「默认值已变, 可显式 task_storage:'file' 回旧行为」; __ 键写存档前剔除
|
|
18
|
+
config.__storageDefaultApplied = true;
|
|
19
|
+
};
|
|
20
|
+
if(TASK_STORAGE_MODES.indexOf(mode) < 0){
|
|
21
|
+
throw new Error(`invalid task_storage "${mode}": must be one of ${TASK_STORAGE_MODES.join('/')}`);
|
|
22
|
+
};
|
|
23
|
+
config.task_storage = mode;
|
|
24
|
+
};
|
|
25
|
+
//resume 链路真实模式判定: 存档 task_storage 键(>=3.5.0 的存档都有)优先; 无键(<3.5.0 旧档)按目录结构嗅探——
|
|
26
|
+
//.sys/aof.log 或 .sys/aof.snapshot.json 存在即 aof, 否则 file。不套 start 的新默认值, 旧 file 档不被默认值变更误伤
|
|
27
|
+
const _sniffTaskStorage = (taskFolder)=>{
|
|
28
|
+
if(fs.existsSync(pathUtil.resolve(taskFolder, '.sys', 'aof.log'))) return 'aof';
|
|
29
|
+
if(fs.existsSync(pathUtil.resolve(taskFolder, '.sys', 'aof.snapshot.json'))) return 'aof';
|
|
30
|
+
return 'file';
|
|
31
|
+
};
|
|
32
|
+
const _readSnapshotTaskStorage = (taskFolder)=>{
|
|
33
|
+
try{
|
|
34
|
+
return JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json'))).task_storage;
|
|
35
|
+
}catch(e){
|
|
36
|
+
return undefined;//快照缺失/损坏由 _mergeSnapshotConfig 统一报错
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const _resolveResumeTaskStorage = (config, userStorage)=>{
|
|
40
|
+
let real = _readSnapshotTaskStorage(config.taskFolder);
|
|
41
|
+
if(typeof real === 'undefined' || real === null){
|
|
42
|
+
real = _sniffTaskStorage(config.taskFolder);
|
|
43
|
+
};
|
|
44
|
+
if(TASK_STORAGE_MODES.indexOf(real) < 0){
|
|
45
|
+
throw new Error(`invalid task_storage "${real}" in task_config.json: must be one of ${TASK_STORAGE_MODES.join('/')}`);
|
|
46
|
+
};
|
|
47
|
+
if(typeof userStorage !== 'undefined' && userStorage !== null){
|
|
48
|
+
if(TASK_STORAGE_MODES.indexOf(userStorage) < 0){
|
|
49
|
+
throw new Error(`invalid task_storage "${userStorage}": must be one of ${TASK_STORAGE_MODES.join('/')}`);
|
|
50
|
+
};
|
|
51
|
+
if(userStorage !== real){
|
|
52
|
+
throw new Error(`task_storage mismatch: task folder was created with task_storage "${real}" but "${userStorage}" was passed; drop task_storage to resume in the folder's own mode`);
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
config.task_storage = real;
|
|
56
|
+
};
|
|
57
|
+
//aof 档周期 checkpoint 配置: 仅 aof 档解析, 避免污染 file/memory 存档。
|
|
58
|
+
//双模式(拍板): aof_checkpoint_ops 显式设置 = 旧固定行数模式(新旧对比开关);
|
|
59
|
+
//未设置 = 字节几何模式(aof_checkpoint_bytes, 语义/默认值见 AofStore);
|
|
60
|
+
//两者 <=0 均关闭周期触发(正常退出时的收尾 checkpoint 不受影响)。缺省不透传, 默认值单一来源在 AofStore。
|
|
61
|
+
const _resolveAofCheckpointConfig = (config)=>{
|
|
62
|
+
if(config.task_storage !== 'aof') return;
|
|
63
|
+
['aof_checkpoint_ops', 'aof_checkpoint_bytes'].forEach((key)=>{
|
|
64
|
+
let value = config[key];
|
|
65
|
+
if(typeof value === 'undefined' || value === null) return;
|
|
66
|
+
if(typeof value !== 'number' || isNaN(value)){
|
|
67
|
+
throw new Error(`invalid ${key} "${value}": must be a number (<=0 disables periodic checkpoint)`);
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
//纯 memory 档目录中无可恢复状态, resume 系 API 调用即 throw;
|
|
72
|
+
//只在 master 进程拦截: worker 进程重跑入口脚本时目录已被 master 创建, 会走 resume 链路拿 __sysLogFile,
|
|
73
|
+
//worker 侧合并配置后不进 init/resume, 不受此限(否则 memory 档 worker 一起步就 throw)
|
|
74
|
+
const _assertMemoryModeResumable = (config)=>{
|
|
75
|
+
if(config.task_storage === 'memory' && (cluster.isPrimary || cluster.isMaster)){
|
|
76
|
+
throw new Error('task_storage "memory" does not persist task state: resume/retry_fails/restart are not available (nothing recoverable in the task folder)');
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const _toAbsPath = (path)=>{
|
|
81
|
+
if(!path) return path;
|
|
82
|
+
if(pathUtil.isAbsolute(path))return path;
|
|
83
|
+
let abspath = pathUtil.resolve(process.cwd(), path);
|
|
84
|
+
return abspath;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const _normalizeResumeArg = (method, config)=>{
|
|
88
|
+
if(typeof config === 'string'){
|
|
89
|
+
throw new Error(`method ${method} need a config object with param "taskFolder" (string path form is no longer supported)`);
|
|
90
|
+
};
|
|
91
|
+
return backwardv1.v1tov2(config);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
//读取任务目录里的 task_config.json 快照, 复活其中的函数字符串后作底;
|
|
95
|
+
//用户传入的同名字段优先(合并结果会随 resume 被 initFolders 重写回快照, 成为新基线)。
|
|
96
|
+
const _mergeSnapshotConfig = (config)=>{
|
|
97
|
+
let snapshot = JSON.parse(fs.readFileSync(pathUtil.resolve(config.taskFolder, 'task_config.json')));
|
|
98
|
+
Object.keys(snapshot).forEach((key)=>{
|
|
99
|
+
if(key.startsWith('__')) delete snapshot[key];//读侧同样剔除一次性标记: 兼容 <=3.1.3 旧版本写出的带标记快照(防 stale 标记误触发 wipe/错走链路)
|
|
100
|
+
});
|
|
101
|
+
let userConfig = { ...config };
|
|
102
|
+
if(typeof userConfig.initialTasks === 'string'){
|
|
103
|
+
delete userConfig.initialTasks;//目录指针用法(auto-resume), 不应覆盖/污染快照里的任务数组
|
|
104
|
+
};
|
|
105
|
+
//用户已传入同名函数时, 快照里的源码字符串无需复活(反正会被覆盖)——容忍不可复活的源码(如绑定函数)
|
|
106
|
+
FUNCTION_CONFIG_KEYS.forEach((key)=>{
|
|
107
|
+
if(typeof userConfig[key] === 'function' && typeof snapshot[key] === 'string'){
|
|
108
|
+
delete snapshot[key];
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
let revived = reviveConfig(snapshot);
|
|
112
|
+
let merged = { ...revived, ...userConfig };
|
|
113
|
+
if(typeof merged.processTask !== 'function'){
|
|
114
|
+
throw new Error('FATAL: no processTask available: task_config.json has none and none was passed in config.');
|
|
115
|
+
};
|
|
116
|
+
return merged;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
//run.log 路径在入口统一算好挂到 config(__ 前缀: 一次性派生值, 不进快照), 与 progressBar 无关;
|
|
120
|
+
//master/worker 各进程都重跑入口、算出同一路径, Multithread 直接使用, 不做任何路径推导。
|
|
121
|
+
const _computeSysLogFile = (config)=>{
|
|
122
|
+
let taskFolder = config.taskFolder || ((config.taskId && config.taskRootFolder) ? pathUtil.resolve(config.taskRootFolder, config.taskId) : null);
|
|
123
|
+
if(taskFolder){
|
|
124
|
+
config.__sysLogFile = pathUtil.resolve(taskFolder, '.sys', 'run.log');
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
//initialTasks 支持函数形式(惰性生成): 只在 master 进程求值一次, worker 进程重跑入口脚本时跳过,
|
|
129
|
+
//防生成逻辑(扫目录/查库等初始化脚本)在每个 worker 里各执行一遍。
|
|
130
|
+
//函数可同步返回数组, 也可返回 Promise(resolve 值同样必须是数组)——同步路径保持同步 throw,
|
|
131
|
+
//Promise 路径返回 then 链交给 _proceedAfterEval 等待。求值在入口统一完成(先于 _mergeSnapshotConfig
|
|
132
|
+
//与 Multithread.start): 求值后的数组才会参与合并并写入 task_config.json(函数本身会被
|
|
133
|
+
//JSON.stringify 丢弃, 不进存档、不参与 revive)。
|
|
134
|
+
const _evalInitialTasks = (config)=>{
|
|
135
|
+
if(typeof config.initialTasks !== 'function') return null;
|
|
136
|
+
if(!(cluster.isPrimary || cluster.isMaster)) return null;
|
|
137
|
+
let tasks = config.initialTasks();
|
|
138
|
+
let applyTasks = (resolved)=>{
|
|
139
|
+
if(!Array.isArray(resolved)){
|
|
140
|
+
throw new Error('FATAL: initialTasks function should return an array!');
|
|
141
|
+
};
|
|
142
|
+
config.initialTasks = resolved;
|
|
143
|
+
};
|
|
144
|
+
if(tasks && typeof tasks.then === 'function'){//Promise 形式: 异步求值, resolve 后同样校验数组(假设真 Promise, 即 .then 返回 Promise; 非标准 thenable 不支持)
|
|
145
|
+
return tasks.then(applyTasks);
|
|
146
|
+
};
|
|
147
|
+
applyTasks(tasks);
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
//同步路径(pending 为 null)直接执行 proceed, 保持既有同步 throw 语义;
|
|
152
|
+
//Promise 路径等 resolve 后执行, resolve 非数组/reject/proceed 抛错时打 FATAL 并 exit(1)
|
|
153
|
+
//(与 init 异步失败一致; Promise 路径只发生在 master, 无同步 throw 对象)。
|
|
154
|
+
const _proceedAfterEval = (pending, proceed)=>{
|
|
155
|
+
if(!pending){
|
|
156
|
+
proceed();
|
|
157
|
+
return;
|
|
158
|
+
};
|
|
159
|
+
return pending.then(proceed).catch((e)=>{
|
|
160
|
+
console.error('[FATAL] failed to evaluate initialTasks:', e);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const start = (config)=>{
|
|
166
|
+
config = backwardv1.v1tov2(config);
|
|
167
|
+
return _proceedAfterEval(_evalInitialTasks(config), ()=>{
|
|
168
|
+
|
|
169
|
+
config.taskRootFolder = _toAbsPath(config.taskRootFolder);
|
|
170
|
+
config.taskFolder = _toAbsPath(config.taskFolder);
|
|
171
|
+
_computeSysLogFile(config);
|
|
172
|
+
_resolveTaskStorage(config);
|
|
173
|
+
_resolveAofCheckpointConfig(config);
|
|
174
|
+
|
|
175
|
+
Multithread.start(config);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const resume = (config)=>{
|
|
179
|
+
config = _normalizeResumeArg('resume', config);
|
|
180
|
+
//先校验/推导 taskFolder 再求值 initialTasks(与 retry_fails/restart 一致):
|
|
181
|
+
//缺目录时同步 throw fail fast, 不让可能昂贵的生成逻辑白跑一遍
|
|
182
|
+
config.taskRootFolder = _toAbsPath(config.taskRootFolder);
|
|
183
|
+
config.taskFolder = _toAbsPath(config.taskFolder);
|
|
184
|
+
if(!config.taskFolder){
|
|
185
|
+
if(!config.taskRootFolder || !config.taskId){
|
|
186
|
+
throw new Error('method resume need param "taskFolder" (or "taskRootFolder" + "taskId")');
|
|
187
|
+
};
|
|
188
|
+
config.taskFolder = pathUtil.resolve(config.taskRootFolder, config.taskId);
|
|
189
|
+
};
|
|
190
|
+
config.taskRootFolder = pathUtil.dirname(config.taskFolder);
|
|
191
|
+
config.taskId = pathUtil.basename(config.taskFolder);
|
|
192
|
+
config.__resume = true;
|
|
193
|
+
return _proceedAfterEval(_evalInitialTasks(config), ()=>{
|
|
194
|
+
_computeSysLogFile(config);
|
|
195
|
+
let userTaskStorage = config.task_storage;
|
|
196
|
+
config = _mergeSnapshotConfig(config);
|
|
197
|
+
_resolveResumeTaskStorage(config, userTaskStorage);
|
|
198
|
+
_resolveAofCheckpointConfig(config);
|
|
199
|
+
_assertMemoryModeResumable(config);
|
|
200
|
+
Multithread.start(config);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const retry_fails = (config)=>{
|
|
204
|
+
config = _normalizeResumeArg('retry_fails', config);
|
|
205
|
+
let taskFolder = _toAbsPath(config.taskFolder);
|
|
206
|
+
if(!taskFolder){
|
|
207
|
+
throw new Error('method retry_fails need param "taskFolder"');
|
|
208
|
+
}
|
|
209
|
+
config.taskFolder = taskFolder;
|
|
210
|
+
config.taskRootFolder = pathUtil.dirname(taskFolder);
|
|
211
|
+
config.taskId = pathUtil.basename(taskFolder);
|
|
212
|
+
config.__resume = true;
|
|
213
|
+
config.__retryFails = true;
|
|
214
|
+
return _proceedAfterEval(_evalInitialTasks(config), ()=>{
|
|
215
|
+
_computeSysLogFile(config);
|
|
216
|
+
let userTaskStorage = config.task_storage;
|
|
217
|
+
config = _mergeSnapshotConfig(config);
|
|
218
|
+
_resolveResumeTaskStorage(config, userTaskStorage);
|
|
219
|
+
_resolveAofCheckpointConfig(config);
|
|
220
|
+
_assertMemoryModeResumable(config);
|
|
221
|
+
Multithread.start(config);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const restart = (config)=>{
|
|
225
|
+
config = _normalizeResumeArg('restart', config);
|
|
226
|
+
let taskFolder = _toAbsPath(config.taskFolder);
|
|
227
|
+
if(!taskFolder){
|
|
228
|
+
throw new Error('method restart need param "taskFolder"');
|
|
229
|
+
}
|
|
230
|
+
config.taskFolder = taskFolder;
|
|
231
|
+
config.taskRootFolder = pathUtil.dirname(taskFolder);
|
|
232
|
+
config.taskId = pathUtil.basename(taskFolder);
|
|
233
|
+
config.__restart = true;
|
|
234
|
+
return _proceedAfterEval(_evalInitialTasks(config), ()=>{
|
|
235
|
+
_computeSysLogFile(config);
|
|
236
|
+
let userTaskStorage = config.task_storage;
|
|
237
|
+
config = _mergeSnapshotConfig(config);
|
|
238
|
+
_resolveResumeTaskStorage(config, userTaskStorage);
|
|
239
|
+
_resolveAofCheckpointConfig(config);
|
|
240
|
+
_assertMemoryModeResumable(config);
|
|
241
|
+
Multithread.start(config);
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const multiTasks = (config)=>{
|
|
246
|
+
config = backwardv1.v1tov2(config);
|
|
247
|
+
let {initialTasks, taskRootFolder, taskId} = config;
|
|
248
|
+
let isResume = false;
|
|
249
|
+
|
|
250
|
+
if(typeof initialTasks === 'string'){//might be an existing tasks folder
|
|
251
|
+
if(fs.existsSync(initialTasks)){
|
|
252
|
+
isResume = true;
|
|
253
|
+
config.taskRootFolder = pathUtil.dirname(initialTasks);
|
|
254
|
+
}else if(taskRootFolder){
|
|
255
|
+
let folder = pathUtil.resolve(taskRootFolder, initialTasks);
|
|
256
|
+
if(fs.existsSync(folder)){
|
|
257
|
+
isResume = true;
|
|
258
|
+
config.taskRootFolder = pathUtil.dirname(folder);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if(!isResume) throw new Error('FATAL: initialTasks should be an array, a function or a task root folder!');
|
|
262
|
+
}else{
|
|
263
|
+
let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
|
|
264
|
+
if(fs.existsSync(taskFolder)){
|
|
265
|
+
isResume = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if(isResume){
|
|
270
|
+
return resume(config);
|
|
271
|
+
}else{
|
|
272
|
+
return start(config);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
multiTasks.start = start;
|
|
278
|
+
multiTasks.retry_fails = retry_fails;
|
|
279
|
+
multiTasks.resume = resume;
|
|
280
|
+
multiTasks.restart = restart;
|
|
281
|
+
|
|
282
|
+
module.exports = multiTasks;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const pathUtil = require('path');
|
|
3
|
+
const runLog = require('../utils/runLog');
|
|
4
|
+
|
|
5
|
+
//内部开关 memory_append_result_files 的 memory 档侧(不对外文档化, 与 aof_checkpoint_* 同属内部调参键):
|
|
6
|
+
// config.memory_append_result_files === true 时把结果行追加到 results/succ.json|errors.json
|
|
7
|
+
// (与 aof 档同款 {subid,data} NDJSON 行 + {subid,deleted:true} 墓碑行, 读者按 subid 取最后一行)。
|
|
8
|
+
// 只追加、收尾不做整体重写(memory 档无 resume, 结果终态出口本来就是 onFinish);
|
|
9
|
+
// 挂上的 flushResults 只关追加流——Multithread 三条退出路径 typeof flushResults==='function'
|
|
10
|
+
// 即 await, 保证流缓冲冲下盘再写计数器行退出。
|
|
11
|
+
const attachResultAppender = (store, taskFolder) => {
|
|
12
|
+
const resultStreams = { succ: null, errors: null };
|
|
13
|
+
let broken = false;
|
|
14
|
+
let warned = false;
|
|
15
|
+
|
|
16
|
+
const appendLine = (hasError, record) => {
|
|
17
|
+
if (broken) return;
|
|
18
|
+
const side = hasError ? 'errors' : 'succ';
|
|
19
|
+
if (!resultStreams[side]) {
|
|
20
|
+
resultStreams[side] = fs.createWriteStream(pathUtil.resolve(taskFolder, 'results', `${side}.json`), { flags: 'a' });
|
|
21
|
+
resultStreams[side].on('error', (e) => {
|
|
22
|
+
broken = true;
|
|
23
|
+
if (!warned) {
|
|
24
|
+
warned = true;
|
|
25
|
+
runLog.warn('[resultFileAppender]: result stream broken, result files degraded for this run', e);
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
resultStreams[side].write(JSON.stringify(record) + '\n');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const rawSaveResult = store.saveResult;
|
|
33
|
+
store.saveResult = (subid, hasError, data) => {
|
|
34
|
+
const result = rawSaveResult(subid, hasError, data);
|
|
35
|
+
appendLine(hasError, typeof data === 'undefined' ? { subid } : { subid, data });
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
const rawDeleteResult = store.deleteResult;
|
|
39
|
+
store.deleteResult = (subid) => {
|
|
40
|
+
const type = store.getSavedResultType(subid);//'succ'/'error'/null: 删前取侧, 墓碑行追加到对应文件
|
|
41
|
+
const result = rawDeleteResult(subid);
|
|
42
|
+
if (type) appendLine(type === 'error', { subid, deleted: true });
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//用 end(cb) 而非等 'close': 流中途出错已销毁时 'close' 早已发完不再重发, 等它会永远悬挂
|
|
47
|
+
//(退出路径 await 本函数, 悬挂 = 挂死 master 收尾); end(cb) 对已销毁流也回调
|
|
48
|
+
store.flushResults = async () => {
|
|
49
|
+
for (const side of ['succ', 'errors']) {
|
|
50
|
+
const s = resultStreams[side];
|
|
51
|
+
if (s) {
|
|
52
|
+
resultStreams[side] = null;
|
|
53
|
+
await new Promise((resolve) => { s.end(resolve); });
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
store.__resultStreams = resultStreams;//测试探针(模拟流中途出错)
|
|
59
|
+
return store;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
module.exports = { attachResultAppender };
|