multi-tasks 3.4.0 → 3.5.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.
@@ -1,102 +1,110 @@
1
- var fs = require('fs');
2
- var pathUtil = require('path');
3
-
4
- const TaskMgr = require('./TaskMgr');
5
- const randomsUtil = require('../utils/randoms');
6
- const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
7
-
8
- const resume = (config)=>{
9
- config.__resume = true;
10
- let {taskRootFolder, taskId} = config;
11
- let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
12
-
13
- //load from config file: 旧快照作底(兼作任务目录合法性校验, 不存在抛 ENOENT),
14
- //本次调用传入的配置覆盖同名字段——快照只补未传入的字段
15
- let task_config = JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json')));
16
- Object.keys(task_config).forEach((key)=>{
17
- if(key.startsWith('__')) delete task_config[key];//读侧剔除一次性标记, 兼容 <=3.1.3 旧快照
18
- });
19
- config = {
20
- ...task_config,
21
- ...config
22
- };
23
- config.masterTaskId = taskId;
24
-
25
- TaskMgr.load(config);
26
- config = TaskMgr.initFolders(config);
27
-
28
- return new Promise((resolve, reject)=>{
29
-
30
- TaskMgr.getTaskFiles('running', (taskfiles)=>{
31
- TaskMgr.getTaskFiles('finished_with_errors', (errfiles)=>{
32
- let promises = [];
33
- taskfiles.forEach((fname)=>{
34
- let subid = fname.replace(/\.task\.json$/, '');
35
- promises.push(TaskMgr.mvTaskPromise(subid, 'running', 'new'));
36
- });
37
- if(config.__retryFails){
38
- errfiles.forEach((fname)=>{
39
- let subid = fname.replace(/\.task\.json$/, '');
40
- promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
41
- TaskMgr.resetTaskRetry(subid);//retry_fails 重跑失败任务时重置重试计数(resume 不重置)
42
- TaskMgr.deleteUserMessage(subid);//重跑即新一轮: 清掉该任务在 .sys/user_messages 的遗留消息
43
- }));
44
- });
45
- };
46
- Promise.all(promises).then(()=>{
47
- TaskMgr.loadAllTasks().then((types)=>{
48
- console.log('******resume multi-thread')
49
- setTimeout(()=>{
50
- resolve(config);
51
- }, 1000);
52
- });
53
-
54
- }).catch((e)=>{
55
- reject(e);
56
- });
57
- });
58
- });
59
- });
60
-
61
-
62
- };
63
-
64
- const init = (config)=>{
65
- if(!config) config = {};
66
- if(!config.initialTasks){ throw new Error('Please provide initialTasks data!'); };
67
- if(!config.processTask){ throw new Error('Please provide processTask function!'); };
68
-
69
- config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
70
-
71
- let {taskRootFolder, taskName} = config;
72
-
73
- if(taskName && !taskName.match(/^[a-zA-Z0-9_-]+$/)){ throw new Error('Error: taskName can only contain letters and numbers'); }
74
- if(!taskName){ taskName = 'multitasks'; }
75
-
76
- let {round, pow, random} = Math;
77
- let dateTimeText = randomsUtil.getDateTimeTxt();
78
- let randomNum = round((random() * pow(10,8)),8).toString(36);
79
- let masterTaskId = config.taskId ? config.taskId : `${taskName}${dateTimeText}--${randomNum}`;
80
-
81
- config.taskRootFolder = taskRootFolder;
82
- config.masterTaskId = masterTaskId;
83
-
84
-
85
- TaskMgr.load(config);
86
- config = TaskMgr.initFolders(config);
87
-
88
- config.initialTasks.forEach((task)=>{
89
- TaskMgr.createNewTask(task);
90
- });
91
-
92
- //to reload is for to reStart a collapsed task
93
- return new Promise((resolve)=>{
94
- TaskMgr.loadNewTasks().then(()=>{
95
- resolve(config);
96
- });
97
- });
98
- };
99
- module.exports = {
100
- init,
101
- resume
1
+ var fs = require('fs');
2
+ var pathUtil = require('path');
3
+
4
+ const TaskMgr = require('./TaskMgr');
5
+ const { createMemoryStore } = require('./MemoryStore');
6
+ const randomsUtil = require('../utils/randoms');
7
+ const { resolveNumberOfWorkers } = require('../utils/resolveWorkers');
8
+
9
+ const resume = (config)=>{
10
+ config.__resume = true;
11
+ let {taskRootFolder, taskId} = config;
12
+ let taskFolder = pathUtil.resolve(taskRootFolder, taskId);
13
+
14
+ //load from config file: 旧存档作底(兼作任务目录合法性校验, 不存在抛 ENOENT),
15
+ //本次调用传入的配置覆盖同名字段——存档只补未传入的字段
16
+ let task_config = JSON.parse(fs.readFileSync(pathUtil.resolve(taskFolder, 'task_config.json')));
17
+ Object.keys(task_config).forEach((key)=>{
18
+ if(key.startsWith('__')) delete task_config[key];//读侧剔除一次性标记, 兼容 <=3.1.3 旧存档
19
+ });
20
+ config = {
21
+ ...task_config,
22
+ ...config
23
+ };
24
+ config.masterTaskId = taskId;
25
+
26
+ TaskMgr.load(config);
27
+ config = TaskMgr.initFolders(config);
28
+
29
+ return new Promise((resolve, reject)=>{
30
+
31
+ TaskMgr.getTaskFiles('running', (taskfiles)=>{
32
+ TaskMgr.getTaskFiles('finished_with_errors', (errfiles)=>{
33
+ let promises = [];
34
+ taskfiles.forEach((fname)=>{
35
+ let subid = fname.replace(/\.task\.json$/, '');
36
+ promises.push(TaskMgr.mvTaskPromise(subid, 'running', 'new'));
37
+ });
38
+ if(config.__retryFails){
39
+ errfiles.forEach((fname)=>{
40
+ let subid = fname.replace(/\.task\.json$/, '');
41
+ promises.push(TaskMgr.mvTaskPromise(subid, 'finished_with_errors', 'new').then(()=>{
42
+ TaskMgr.resetTaskRetry(subid);//retry_fails 重跑失败任务时重置重试计数(resume 不重置)
43
+ TaskMgr.deleteUserMessage(subid);//重跑即新一轮: 清掉该任务在 .sys/user_messages 的遗留消息
44
+ }));
45
+ });
46
+ };
47
+ Promise.all(promises).then(()=>{
48
+ TaskMgr.loadAllTasks().then((types)=>{
49
+ console.log('******resume multi-thread')
50
+ setTimeout(()=>{
51
+ resolve({config, store: null});//memory 档 resume 在 index.js 已被拦截 throw, 到这里的都是 file 档
52
+ }, 1000);
53
+ });
54
+
55
+ }).catch((e)=>{
56
+ reject(e);
57
+ });
58
+ });
59
+ });
60
+ });
61
+
62
+
63
+ };
64
+
65
+ const init = (config)=>{
66
+ if(!config) config = {};
67
+ if(!config.initialTasks){ throw new Error('Please provide initialTasks data!'); };
68
+ if(!config.processTask){ throw new Error('Please provide processTask function!'); };
69
+
70
+ config.numberOfWorkers = resolveNumberOfWorkers(config.numberOfWorkers);//支持百分比字符串, 非法值在此抛错(fail fast, 不建任务目录)
71
+
72
+ let {taskRootFolder, taskName} = config;
73
+
74
+ if(taskName && !taskName.match(/^[a-zA-Z0-9_-]+$/)){ throw new Error('Error: taskName can only contain letters and numbers'); }
75
+ if(!taskName){ taskName = 'multitasks'; }
76
+
77
+ let {round, pow, random} = Math;
78
+ let dateTimeText = randomsUtil.getDateTimeTxt();
79
+ let randomNum = round((random() * pow(10,8)),8).toString(36);
80
+ let masterTaskId = config.taskId ? config.taskId : `${taskName}${dateTimeText}--${randomNum}`;
81
+
82
+ config.taskRootFolder = taskRootFolder;
83
+ config.masterTaskId = masterTaskId;
84
+
85
+
86
+ TaskMgr.load(config);
87
+ config = TaskMgr.initFolders(config);
88
+
89
+ if(config.task_storage === 'memory'){
90
+ //memory 档: 不建任务文件(目录与 task_config.json 照建照写), 播种进 master 内存存储
91
+ const store = createMemoryStore(config);
92
+ store.addMany(config.initialTasks);
93
+ return Promise.resolve({config, store});
94
+ };
95
+
96
+ config.initialTasks.forEach((task)=>{
97
+ TaskMgr.createNewTask(task);
98
+ });
99
+
100
+ //to reload is for to reStart a collapsed task
101
+ return new Promise((resolve)=>{
102
+ TaskMgr.loadNewTasks().then(()=>{
103
+ resolve({config, store: null});
104
+ });
105
+ });
106
+ };
107
+ module.exports = {
108
+ init,
109
+ resume
102
110
  };
package/workers/helper.js CHANGED
@@ -1,67 +1,77 @@
1
- let fs = require('fs');
2
- let pathutil = require('path');
3
-
4
- let TaskMgr = require('./TaskMgr');
5
- let runLog = require('../utils/runLog');
6
-
7
- let myconfig;
8
- let listeners = {};//event -> handler, worker 进程内模块级广播监听 registry(每个 event 一个 handler)
9
-
10
- const load = (config)=>{
11
- myconfig = config;
12
- };
13
- const createNewTasks = (tasks)=>{
14
- if (!myconfig) throw new Error('Please call helper.load(config) first!');
15
- if (!Array.isArray(tasks)) tasks = [tasks];
16
- TaskMgr.load(myconfig);
17
- tasks.forEach((task)=>{
18
- runLog.write('Creating new task', task)
19
- TaskMgr.createNewTask(task);
20
- });
21
- };
22
-
23
- //广播事件给所有 worker(经 master 中转); option.includingMe 默认 true(也发给自己)
24
- const emit = (event, payload, option)=>{
25
- if (typeof event !== 'string' || !event) throw new Error('helper.emit: event must be a non-empty string');
26
- if (typeof process.send !== 'function') throw new Error('helper.emit: broadcast is only available in worker processes');
27
- let includingMe = !option || option.includingMe !== false;
28
- process.send({__broadcast: true, event, payload, includingMe});
29
- };
30
- //发送系统事件给 master(master 自己消费, 不转发); 内建事件 TERMINATE_ALL_WORKERS,
31
- //其余交给 config.setSysListener
32
- const emitSys = (event, payload)=>{
33
- if (typeof event !== 'string' || !event) throw new Error('helper.emitSys: event must be a non-empty string');
34
- if (typeof process.send !== 'function') throw new Error('helper.emitSys: system events are only available in worker processes');
35
- process.send({__sysEvent: true, event, payload});
36
- };
37
- //监听广播事件, 每个 event 每个 worker 进程只保留一个 handler(重复注册覆盖),
38
- //handler 签名 (payload, meta), meta 含 fromWorkerId/fromWorkerPid
39
- const setListener = (event, handler)=>{
40
- if (typeof event !== 'string' || !event) throw new Error('helper.setListener: event must be a non-empty string');
41
- if (typeof handler !== 'function') throw new Error('helper.setListener: handler must be a function');
42
- listeners[event] = handler;
43
- };
44
- //任务日志: 一律写 <taskFolder>/.sys/run.log; progressBar 未开/非 TTY 回退时 echo 上屏(效果等同 console.log)
45
- const log = (...args)=>{
46
- runLog.write(...args);
47
- };
48
- //内部方法: subWorker 收到 master 中转的广播时调用, 按 event 分发
49
- const __dispatchBroadcast = (message)=>{
50
- let {event, payload, meta} = message;
51
- let handler = listeners[event];
52
- if (!handler) return;
53
- try{
54
- handler(payload, meta);
55
- }catch(e){
56
- runLog.error(`[Broadcast] handler for event "${event}" threw:`, e);
57
- }
58
- };
59
- module.exports = {
60
- load,
61
- createNewTasks,
62
- emit,
63
- emitSys,
64
- setListener,
65
- log,
66
- __dispatchBroadcast
67
- };
1
+ let fs = require('fs');
2
+ let pathutil = require('path');
3
+
4
+ let TaskMgr = require('./TaskMgr');
5
+ let IpcTaskStore = require('./IpcTaskStore');
6
+ let runLog = require('../utils/runLog');
7
+
8
+ let myconfig;
9
+ let listeners = {};//event -> handler, worker 进程内模块级广播监听 registry(每个 event 一个 handler)
10
+
11
+ const load = (config)=>{
12
+ myconfig = config;
13
+ };
14
+ const createNewTasks = (tasks)=>{
15
+ if (!myconfig) throw new Error('Please call helper.load(config) first!');
16
+ if (!Array.isArray(tasks)) tasks = [tasks];
17
+ if(myconfig.task_storage === 'memory'){
18
+ //memory worker 不碰盘: 每个动态任务一条 createTask 消息即时发 master(逐条同构, 禁止缓冲攒批)
19
+ IpcTaskStore.load(myconfig);
20
+ tasks.forEach((task)=>{
21
+ runLog.write('Creating new task', task)
22
+ IpcTaskStore.createNewTask(task);
23
+ });
24
+ return;
25
+ };
26
+ TaskMgr.load(myconfig);
27
+ tasks.forEach((task)=>{
28
+ runLog.write('Creating new task', task)
29
+ TaskMgr.createNewTask(task);
30
+ });
31
+ };
32
+
33
+ //广播事件给所有 worker( master 中转); option.includingMe 默认 true(也发给自己)
34
+ const emit = (event, payload, option)=>{
35
+ if (typeof event !== 'string' || !event) throw new Error('helper.emit: event must be a non-empty string');
36
+ if (typeof process.send !== 'function') throw new Error('helper.emit: broadcast is only available in worker processes');
37
+ let includingMe = !option || option.includingMe !== false;
38
+ process.send({__broadcast: true, event, payload, includingMe});
39
+ };
40
+ //发送系统事件给 master(master 自己消费, 不转发); 内建事件 TERMINATE_ALL_WORKERS,
41
+ //其余交给 config.setSysListener
42
+ const emitSys = (event, payload)=>{
43
+ if (typeof event !== 'string' || !event) throw new Error('helper.emitSys: event must be a non-empty string');
44
+ if (typeof process.send !== 'function') throw new Error('helper.emitSys: system events are only available in worker processes');
45
+ process.send({__sysEvent: true, event, payload});
46
+ };
47
+ //监听广播事件, 每个 event 每个 worker 进程只保留一个 handler(重复注册覆盖),
48
+ //handler 签名 (payload, meta), meta fromWorkerId/fromWorkerPid
49
+ const setListener = (event, handler)=>{
50
+ if (typeof event !== 'string' || !event) throw new Error('helper.setListener: event must be a non-empty string');
51
+ if (typeof handler !== 'function') throw new Error('helper.setListener: handler must be a function');
52
+ listeners[event] = handler;
53
+ };
54
+ //任务日志: 一律写 <taskFolder>/.sys/run.log; progressBar 未开/非 TTY 回退时 echo 上屏(效果等同 console.log)
55
+ const log = (...args)=>{
56
+ runLog.write(...args);
57
+ };
58
+ //内部方法: 供 subWorker 收到 master 中转的广播时调用, 按 event 分发
59
+ const __dispatchBroadcast = (message)=>{
60
+ let {event, payload, meta} = message;
61
+ let handler = listeners[event];
62
+ if (!handler) return;
63
+ try{
64
+ handler(payload, meta);
65
+ }catch(e){
66
+ runLog.error(`[Broadcast] handler for event "${event}" threw:`, e);
67
+ }
68
+ };
69
+ module.exports = {
70
+ load,
71
+ createNewTasks,
72
+ emit,
73
+ emitSys,
74
+ setListener,
75
+ log,
76
+ __dispatchBroadcast
77
+ };
package/workers/index.js CHANGED
@@ -5,6 +5,27 @@ const Multithread = require('./Multithread');
5
5
  const backwardv1 = require('../utils/backward-v1');
6
6
  const { FUNCTION_CONFIG_KEYS, reviveConfig } = require('../utils/serializeFns');
7
7
 
8
+ const TASK_STORAGE_MODES = ['file', 'memory'];
9
+ //task_storage 两档: file(默认, 任务状态实时写磁盘, 可 resume) / memory(纯内存, 进程退出即丢)
10
+ const _resolveTaskStorage = (config)=>{
11
+ let mode = config.task_storage;
12
+ if(typeof mode === 'undefined' || mode === null){
13
+ mode = 'file';
14
+ };
15
+ if(TASK_STORAGE_MODES.indexOf(mode) < 0){
16
+ throw new Error(`invalid task_storage "${mode}": must be one of ${TASK_STORAGE_MODES.join('/')}`);
17
+ };
18
+ config.task_storage = mode;
19
+ };
20
+ //纯 memory 档目录中无可恢复状态, resume 系 API 调用即 throw;
21
+ //只在 master 进程拦截: worker 进程重跑入口脚本时目录已被 master 创建, 会走 resume 链路拿 __sysLogFile,
22
+ //worker 侧合并配置后不进 init/resume, 不受此限(否则 memory 档 worker 一起步就 throw)
23
+ const _assertMemoryModeResumable = (config)=>{
24
+ if(config.task_storage === 'memory' && (cluster.isPrimary || cluster.isMaster)){
25
+ throw new Error('task_storage "memory" does not persist task state: resume/retry_fails/restart are not available (nothing recoverable in the task folder)');
26
+ };
27
+ };
28
+
8
29
  const _toAbsPath = (path)=>{
9
30
  if(!path) return path;
10
31
  if(pathUtil.isAbsolute(path))return path;
@@ -97,6 +118,7 @@ const start = (config)=>{
97
118
  config.taskRootFolder = _toAbsPath(config.taskRootFolder);
98
119
  config.taskFolder = _toAbsPath(config.taskFolder);
99
120
  _computeSysLogFile(config);
121
+ _resolveTaskStorage(config);
100
122
 
101
123
  Multithread.start(config);
102
124
  });
@@ -119,6 +141,8 @@ const resume = (config)=>{
119
141
  return _proceedAfterEval(_evalInitialTasks(config), ()=>{
120
142
  _computeSysLogFile(config);
121
143
  config = _mergeSnapshotConfig(config);
144
+ _resolveTaskStorage(config);
145
+ _assertMemoryModeResumable(config);
122
146
  Multithread.start(config);
123
147
  });
124
148
  };
@@ -136,6 +160,8 @@ const retry_fails = (config)=>{
136
160
  return _proceedAfterEval(_evalInitialTasks(config), ()=>{
137
161
  _computeSysLogFile(config);
138
162
  config = _mergeSnapshotConfig(config);
163
+ _resolveTaskStorage(config);
164
+ _assertMemoryModeResumable(config);
139
165
  Multithread.start(config);
140
166
  });
141
167
  };
@@ -152,6 +178,8 @@ const restart = (config)=>{
152
178
  return _proceedAfterEval(_evalInitialTasks(config), ()=>{
153
179
  _computeSysLogFile(config);
154
180
  config = _mergeSnapshotConfig(config);
181
+ _resolveTaskStorage(config);
182
+ _assertMemoryModeResumable(config);
155
183
  Multithread.start(config);
156
184
  });
157
185
  };