echoapi-cron-scheduler-batch 1.0.4 → 1.0.5
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/index.js +215 -107
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const { execSync } = require('child_process');
|
|
|
16
16
|
class CronScheduler {
|
|
17
17
|
constructor(config = {}) {
|
|
18
18
|
// 配置初始化
|
|
19
|
-
this.apiUrl = config.apiUrl || process?.env["OPENAPI_DOMAIN"] || "https://
|
|
19
|
+
this.apiUrl = config.apiUrl || process?.env["OPENAPI_DOMAIN"] || "https://ee.apipost.cc";
|
|
20
20
|
this.dbFile = config.dbPath || path.resolve(os.tmpdir(), 'echoapi-batch-tasks.sqlite');
|
|
21
21
|
this.workerNum = config.workerNum || os.cpus().length;
|
|
22
22
|
|
|
@@ -33,6 +33,8 @@ class CronScheduler {
|
|
|
33
33
|
* 内部方法:初始化数据库表结构
|
|
34
34
|
*/
|
|
35
35
|
_initDB() {
|
|
36
|
+
// 1. 任务配置表
|
|
37
|
+
|
|
36
38
|
this.db.pragma('journal_mode = WAL');
|
|
37
39
|
this.db.prepare(`
|
|
38
40
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
@@ -46,6 +48,17 @@ class CronScheduler {
|
|
|
46
48
|
create_dtime INTEGER
|
|
47
49
|
)
|
|
48
50
|
`).run();
|
|
51
|
+
|
|
52
|
+
// 2. 新增:中间结果结果暂存表 (用于卸载 Master 内存压力)
|
|
53
|
+
this.db.prepare(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS task_results (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
execution_id TEXT,
|
|
57
|
+
data TEXT,
|
|
58
|
+
create_at INTEGER
|
|
59
|
+
)
|
|
60
|
+
`).run();
|
|
61
|
+
this.db.prepare(`CREATE INDEX IF NOT EXISTS idx_exec_id ON task_results(execution_id)`).run();
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
/**
|
|
@@ -100,14 +113,29 @@ class CronScheduler {
|
|
|
100
113
|
|
|
101
114
|
if (record) {
|
|
102
115
|
|
|
103
|
-
record.received.push(_.assign(data, {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}));
|
|
108
|
-
// 检查是否收齐了该 Job 下所有的 runners 结果
|
|
109
|
-
if (record.received.length >= record.total) {
|
|
110
|
-
|
|
116
|
+
// record.received.push(_.assign(data, {
|
|
117
|
+
// source: 'scheduled',
|
|
118
|
+
// start_at: this.formatTimeToISO(data?.start_at),
|
|
119
|
+
// end_at: this.formatTimeToISO(data?.end_at)
|
|
120
|
+
// }));
|
|
121
|
+
// // 检查是否收齐了该 Job 下所有的 runners 结果
|
|
122
|
+
// if (record.received.length >= record.total) {
|
|
123
|
+
// await this._reportAggregated(executionId, record);
|
|
124
|
+
// }
|
|
125
|
+
try {
|
|
126
|
+
// 【关键优化】不再存入内存 Map,而是存入 SQLite 临时表
|
|
127
|
+
this.db.prepare(`INSERT INTO task_results (execution_id, data, create_at) VALUES (?, ?, ?)`)
|
|
128
|
+
.run(executionId, JSON.stringify(data), Date.now());
|
|
129
|
+
|
|
130
|
+
record.received_count++;
|
|
131
|
+
console.log(record.total, record.received_count, 'record.received_count');
|
|
132
|
+
|
|
133
|
+
// 检查是否收齐
|
|
134
|
+
if (record.received_count >= record.total) {
|
|
135
|
+
await this._reportFromDB(executionId, record);
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(`[Master Result Collect Error]`, e.message);
|
|
111
139
|
}
|
|
112
140
|
}
|
|
113
141
|
}
|
|
@@ -117,103 +145,103 @@ class CronScheduler {
|
|
|
117
145
|
/**
|
|
118
146
|
* 将聚合后的结果一次性推送到 OpenAPI
|
|
119
147
|
*/
|
|
120
|
-
async _reportAggregated(executionId, record) {
|
|
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
|
-
}
|
|
148
|
+
// async _reportAggregated(executionId, record) {
|
|
149
|
+
// try {
|
|
150
|
+
|
|
151
|
+
// const results = record.received;
|
|
152
|
+
// const endTime = Date.now();
|
|
153
|
+
|
|
154
|
+
// // --- 核心统计逻辑 ---
|
|
155
|
+
// let stats = {
|
|
156
|
+
// http_total: 0,
|
|
157
|
+
// http_success: 0,
|
|
158
|
+
// assert_total: 0,
|
|
159
|
+
// assert_success: 0,
|
|
160
|
+
// total_response_time: 0,
|
|
161
|
+
// count: 0
|
|
162
|
+
// };
|
|
163
|
+
|
|
164
|
+
// results.forEach(item => {
|
|
165
|
+
// // 累加 HTTP 统计
|
|
166
|
+
// stats.http_total += (item.http?.total || 0);
|
|
167
|
+
// stats.http_success += (item.http?.success || 0);
|
|
168
|
+
|
|
169
|
+
// // 累加 断言 统计
|
|
170
|
+
// stats.assert_total += (item.assert?.total || 0);
|
|
171
|
+
// stats.assert_success += (item.assert?.success || 0);
|
|
172
|
+
|
|
173
|
+
// // 累加 响应时间 用于计算平均值
|
|
174
|
+
// stats.total_response_time += (item.total_response_time || 0);
|
|
175
|
+
// stats.count++;
|
|
176
|
+
// });
|
|
177
|
+
|
|
178
|
+
// // 计算通过率 (保留两位小数)
|
|
179
|
+
// const httpPassRate = stats.http_total > 0
|
|
180
|
+
// ? ((stats.http_success / stats.http_total) * 100).toFixed(2)
|
|
181
|
+
// : "0.00";
|
|
182
|
+
|
|
183
|
+
// const assertPassRate = stats.assert_total > 0
|
|
184
|
+
// ? ((stats.assert_success / stats.assert_total) * 100).toFixed(2)
|
|
185
|
+
// : "0.00";
|
|
186
|
+
|
|
187
|
+
// // 计算平均响应时间
|
|
188
|
+
// const avgResponseTime = stats.count > 0
|
|
189
|
+
// ? (stats.total_response_time / stats.count).toFixed(0)
|
|
190
|
+
// : 0;
|
|
191
|
+
|
|
192
|
+
// // --- 封装最终 Payload ---
|
|
193
|
+
// const finalPayload = {
|
|
194
|
+
// info: {
|
|
195
|
+
// job_id: record.job_info.job_id,
|
|
196
|
+
// report_name: record.job_info.name,
|
|
197
|
+
// project_id: record.job_info.project_id,
|
|
198
|
+
// execution_id: executionId,
|
|
199
|
+
// start_at: this.formatTimeToISO(record.job_info.start_time),
|
|
200
|
+
// end_at: this.formatTimeToISO(endTime),
|
|
201
|
+
|
|
202
|
+
// // 业务统计字段
|
|
203
|
+
// http_pass_rate: `${httpPassRate}%`,
|
|
204
|
+
// assert_pass_rate: `${assertPassRate}%`,
|
|
205
|
+
// avg_response_time: `${avgResponseTime}ms`,
|
|
206
|
+
|
|
207
|
+
// // 原始计数
|
|
208
|
+
// total_http: stats.http_total,
|
|
209
|
+
// success_http: stats.http_success,
|
|
210
|
+
// total_assert: stats.assert_total,
|
|
211
|
+
// success_assert: stats.assert_success,
|
|
212
|
+
|
|
213
|
+
// total_units: record.total,
|
|
214
|
+
// actual_units: results.length
|
|
215
|
+
// },
|
|
216
|
+
// results: results // 原始详细列表
|
|
217
|
+
// };
|
|
218
|
+
|
|
219
|
+
// // 写入本地备份,增加 try-catch 避免权限问题
|
|
220
|
+
// try {
|
|
221
|
+
// // fs.writeFileSync('last_report.json', JSON.stringify(finalPayload));
|
|
222
|
+
// } catch (e) { }
|
|
223
|
+
|
|
224
|
+
// console.log(`[CronScheduler] Batch job finished. ExecutionID: ${executionId}. Aggregating ${record.total} results...`);
|
|
225
|
+
// // console.log(`[CronScheduler] TOKEN ${record.api_token}`);
|
|
226
|
+
|
|
227
|
+
// const response = await axios.post(`${this.apiUrl}/open/hnzycfc/scheduled_task/report/add`, finalPayload, {
|
|
228
|
+
// headers: {
|
|
229
|
+
// 'Content-Type': 'application/json',
|
|
230
|
+
// 'api-token': record.api_token
|
|
231
|
+
// }
|
|
232
|
+
// });
|
|
233
|
+
// if (response?.data?.code != 0) {
|
|
234
|
+
// console.log(`Execution Id ${executionId} reporting error: ${response?.data?.msg}`);
|
|
235
|
+
// } else {
|
|
236
|
+
// console.log(`Execution Id ${executionId} reported success...`);
|
|
237
|
+
// }
|
|
238
|
+
// } catch (err) {
|
|
239
|
+
// console.error(`[CronScheduler] Report aggregation failed:`, err.message);
|
|
240
|
+
// } finally {
|
|
241
|
+
// this.resultCollector.delete(executionId); // 清理内存
|
|
242
|
+
// console.info(`[CronScheduler] Result collector cleared for ${executionId}`);
|
|
243
|
+
// }
|
|
244
|
+
// }
|
|
217
245
|
/**
|
|
218
246
|
* 查看当前所有待执行(活跃)的任务及其详细信息
|
|
219
247
|
*/
|
|
@@ -375,6 +403,86 @@ class CronScheduler {
|
|
|
375
403
|
});
|
|
376
404
|
}
|
|
377
405
|
|
|
406
|
+
/**
|
|
407
|
+
* 从数据库提取数据并聚合报告
|
|
408
|
+
*/
|
|
409
|
+
async _reportFromDB(executionId, record) {
|
|
410
|
+
try {
|
|
411
|
+
console.log(`[CronScheduler] Batch job finished. Aggregating results from DB for: ${executionId}`);
|
|
412
|
+
|
|
413
|
+
// 1. 从 DB 读取该执行批次的所有结果
|
|
414
|
+
const rows = this.db.prepare(`SELECT data FROM task_results WHERE execution_id = ?`).all(executionId);
|
|
415
|
+
|
|
416
|
+
let stats = {
|
|
417
|
+
http_total: 0, http_success: 0,
|
|
418
|
+
assert_total: 0, assert_success: 0,
|
|
419
|
+
total_resp_time: 0
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// 2. 转换数据并计算统计信息
|
|
423
|
+
const results = rows.map(row => {
|
|
424
|
+
const item = JSON.parse(row.data);
|
|
425
|
+
|
|
426
|
+
// 累加统计
|
|
427
|
+
stats.http_total += (item.http?.total || 0);
|
|
428
|
+
stats.http_success += (item.http?.success || 0);
|
|
429
|
+
stats.assert_total += (item.assert?.total || 0);
|
|
430
|
+
stats.assert_success += (item.assert?.success || 0);
|
|
431
|
+
stats.total_resp_time += (item.total_response_time || 0);
|
|
432
|
+
|
|
433
|
+
return _.assign(item, {
|
|
434
|
+
source: 'scheduled',
|
|
435
|
+
start_at: this.formatTimeToISO(item?.start_at),
|
|
436
|
+
end_at: this.formatTimeToISO(item?.end_at)
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// 3. 计算通过率等指标
|
|
441
|
+
const httpRate = stats.http_total > 0 ? ((stats.http_success / stats.http_total) * 100).toFixed(2) : "0.00";
|
|
442
|
+
const assertRate = stats.assert_total > 0 ? ((stats.assert_success / stats.assert_total) * 100).toFixed(2) : "0.00";
|
|
443
|
+
const avgResp = results.length > 0 ? (stats.total_resp_time / results.length).toFixed(0) : 0;
|
|
444
|
+
|
|
445
|
+
// 4. 封装 Payload
|
|
446
|
+
const finalPayload = {
|
|
447
|
+
info: {
|
|
448
|
+
job_id: record.job_info.job_id,
|
|
449
|
+
report_name: record.job_info.name,
|
|
450
|
+
project_id: record.job_info.project_id,
|
|
451
|
+
user_uid: record.job_info.user_uid,
|
|
452
|
+
execution_id: executionId,
|
|
453
|
+
start_at: this.formatTimeToISO(record.job_info.start_time),
|
|
454
|
+
end_at: this.formatTimeToISO(Date.now()),
|
|
455
|
+
http_pass_rate: `${httpRate}%`,
|
|
456
|
+
assert_pass_rate: `${assertRate}%`,
|
|
457
|
+
avg_response_time: `${avgResp}ms`,
|
|
458
|
+
total_units: record.total,
|
|
459
|
+
actual_units: results.length
|
|
460
|
+
},
|
|
461
|
+
results: results
|
|
462
|
+
};
|
|
463
|
+
// fs.writeFileSync('finalPayload.json', JSON.stringify(finalPayload))
|
|
464
|
+
// 5. 上报 API
|
|
465
|
+
const response = await axios.post(`${this.apiUrl}/open/hnzycfc/scheduled_task/report/add`, finalPayload, {
|
|
466
|
+
headers: { 'Content-Type': 'application/json', 'api-token': record.api_token },
|
|
467
|
+
maxContentLength: Infinity,
|
|
468
|
+
maxBodyLength: Infinity
|
|
469
|
+
});
|
|
470
|
+
// console.log(response?.data, 'response?.data');
|
|
471
|
+
|
|
472
|
+
if (response?.data?.code == 0) {
|
|
473
|
+
console.log(`[CronScheduler] Execution ${executionId} reported success.`);
|
|
474
|
+
// 成功后删除数据库里的临时结果
|
|
475
|
+
this.db.prepare(`DELETE FROM task_results WHERE execution_id = ?`).run(executionId);
|
|
476
|
+
} else {
|
|
477
|
+
console.error(`[Report Error] ${executionId}: ${response?.data?.msg}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.error(`[Aggregation Critical Error]`, err.stack);
|
|
482
|
+
} finally {
|
|
483
|
+
this.resultCollector.delete(executionId);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
378
486
|
/**
|
|
379
487
|
* 分发逻辑:将一个批次内的所有 Runner 均衡分配给 Workers
|
|
380
488
|
*/
|
|
@@ -386,7 +494,7 @@ class CronScheduler {
|
|
|
386
494
|
const executionId = `${job.job_id}_${Date.now()}`;
|
|
387
495
|
this.resultCollector.set(executionId, {
|
|
388
496
|
total: runners.length,
|
|
389
|
-
|
|
497
|
+
received_count: 0, // 仅计数
|
|
390
498
|
api_token: job.api_token,
|
|
391
499
|
job_info: {
|
|
392
500
|
job_id: job.job_id,
|