@qwe8652591/abap-recursive-query 1.1.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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/cli/consumer-cli.d.ts +8 -0
- package/dist/cli/consumer-cli.d.ts.map +1 -0
- package/dist/cli/consumer-cli.js +180 -0
- package/dist/cli/producer-cli.d.ts +8 -0
- package/dist/cli/producer-cli.d.ts.map +1 -0
- package/dist/cli/producer-cli.js +249 -0
- package/dist/cli/query-object.d.ts +3 -0
- package/dist/cli/query-object.d.ts.map +1 -0
- package/dist/cli/query-object.js +486 -0
- package/dist/common/FilterCache.d.ts +62 -0
- package/dist/common/FilterCache.d.ts.map +1 -0
- package/dist/common/FilterCache.js +119 -0
- package/dist/common/RedisQueueManager.d.ts +170 -0
- package/dist/common/RedisQueueManager.d.ts.map +1 -0
- package/dist/common/RedisQueueManager.js +663 -0
- package/dist/common/abapStructures.d.ts +391 -0
- package/dist/common/abapStructures.d.ts.map +1 -0
- package/dist/common/abapStructures.js +2 -0
- package/dist/common/config.d.ts +66 -0
- package/dist/common/config.d.ts.map +1 -0
- package/dist/common/config.js +56 -0
- package/dist/common/index.d.ts +13 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +38 -0
- package/dist/common/recursiveQueryConfig.d.ts +77 -0
- package/dist/common/recursiveQueryConfig.d.ts.map +1 -0
- package/dist/common/recursiveQueryConfig.js +129 -0
- package/dist/common/tableStructures.d.ts +176 -0
- package/dist/common/tableStructures.d.ts.map +1 -0
- package/dist/common/tableStructures.js +10 -0
- package/dist/common/types.d.ts +104 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +31 -0
- package/dist/common/utils.d.ts +57 -0
- package/dist/common/utils.d.ts.map +1 -0
- package/dist/common/utils.js +300 -0
- package/dist/consumer/FileDownloadConsumer.d.ts +127 -0
- package/dist/consumer/FileDownloadConsumer.d.ts.map +1 -0
- package/dist/consumer/FileDownloadConsumer.js +1003 -0
- package/dist/consumer/generators/baseGenerator.d.ts +38 -0
- package/dist/consumer/generators/baseGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/baseGenerator.js +103 -0
- package/dist/consumer/generators/domainGenerator.d.ts +78 -0
- package/dist/consumer/generators/domainGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/domainGenerator.js +241 -0
- package/dist/consumer/generators/guiStatusGenerator.d.ts +16 -0
- package/dist/consumer/generators/guiStatusGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/guiStatusGenerator.js +48 -0
- package/dist/consumer/generators/guiTitleGenerator.d.ts +16 -0
- package/dist/consumer/generators/guiTitleGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/guiTitleGenerator.js +48 -0
- package/dist/consumer/generators/index.d.ts +14 -0
- package/dist/consumer/generators/index.d.ts.map +1 -0
- package/dist/consumer/generators/index.js +38 -0
- package/dist/consumer/generators/messageGenerator.d.ts +16 -0
- package/dist/consumer/generators/messageGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/messageGenerator.js +73 -0
- package/dist/consumer/generators/metadataGenerator.d.ts +29 -0
- package/dist/consumer/generators/metadataGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/metadataGenerator.js +135 -0
- package/dist/consumer/generators/screenGenerator.d.ts +173 -0
- package/dist/consumer/generators/screenGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/screenGenerator.js +859 -0
- package/dist/consumer/generators/structureGenerator.d.ts +36 -0
- package/dist/consumer/generators/structureGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/structureGenerator.js +131 -0
- package/dist/consumer/generators/textElementGenerator.d.ts +16 -0
- package/dist/consumer/generators/textElementGenerator.d.ts.map +1 -0
- package/dist/consumer/generators/textElementGenerator.js +70 -0
- package/dist/consumer/handlers/handleGetClass.d.ts +8 -0
- package/dist/consumer/handlers/handleGetClass.d.ts.map +1 -0
- package/dist/consumer/handlers/handleGetClass.js +18 -0
- package/dist/consumer/handlers/handleGetFunction.d.ts +8 -0
- package/dist/consumer/handlers/handleGetFunction.d.ts.map +1 -0
- package/dist/consumer/handlers/handleGetFunction.js +19 -0
- package/dist/consumer/handlers/handleGetFunctionGroup.d.ts +8 -0
- package/dist/consumer/handlers/handleGetFunctionGroup.d.ts.map +1 -0
- package/dist/consumer/handlers/handleGetFunctionGroup.js +19 -0
- package/dist/consumer/handlers/handleGetInclude.d.ts +8 -0
- package/dist/consumer/handlers/handleGetInclude.d.ts.map +1 -0
- package/dist/consumer/handlers/handleGetInclude.js +18 -0
- package/dist/consumer/handlers/handleGetProgram.d.ts +8 -0
- package/dist/consumer/handlers/handleGetProgram.d.ts.map +1 -0
- package/dist/consumer/handlers/handleGetProgram.js +27 -0
- package/dist/consumer/handlers/handleZTableQuery.d.ts +8 -0
- package/dist/consumer/handlers/handleZTableQuery.d.ts.map +1 -0
- package/dist/consumer/handlers/handleZTableQuery.js +20 -0
- package/dist/consumer/handlers/index.d.ts +10 -0
- package/dist/consumer/handlers/index.d.ts.map +1 -0
- package/dist/consumer/handlers/index.js +27 -0
- package/dist/consumer/index.d.ts +9 -0
- package/dist/consumer/index.d.ts.map +1 -0
- package/dist/consumer/index.js +24 -0
- package/dist/consumer/utils/download.d.ts +13 -0
- package/dist/consumer/utils/download.d.ts.map +1 -0
- package/dist/consumer/utils/download.js +38 -0
- package/dist/consumer/utils/index.d.ts +5 -0
- package/dist/consumer/utils/index.d.ts.map +1 -0
- package/dist/consumer/utils/index.js +20 -0
- package/dist/handlers/handleGetClass.d.ts +8 -0
- package/dist/handlers/handleGetClass.d.ts.map +1 -0
- package/dist/handlers/handleGetClass.js +19 -0
- package/dist/handlers/handleGetFunction.d.ts +8 -0
- package/dist/handlers/handleGetFunction.d.ts.map +1 -0
- package/dist/handlers/handleGetFunction.js +20 -0
- package/dist/handlers/handleGetFunctionGroup.d.ts +8 -0
- package/dist/handlers/handleGetFunctionGroup.d.ts.map +1 -0
- package/dist/handlers/handleGetFunctionGroup.js +19 -0
- package/dist/handlers/handleGetInclude.d.ts +8 -0
- package/dist/handlers/handleGetInclude.d.ts.map +1 -0
- package/dist/handlers/handleGetInclude.js +19 -0
- package/dist/handlers/handleGetInterface.d.ts +8 -0
- package/dist/handlers/handleGetInterface.d.ts.map +1 -0
- package/dist/handlers/handleGetInterface.js +19 -0
- package/dist/handlers/handleGetPackage.d.ts +8 -0
- package/dist/handlers/handleGetPackage.d.ts.map +1 -0
- package/dist/handlers/handleGetPackage.js +42 -0
- package/dist/handlers/handleGetProgram.d.ts +8 -0
- package/dist/handlers/handleGetProgram.d.ts.map +1 -0
- package/dist/handlers/handleGetProgram.js +19 -0
- package/dist/handlers/handleGetStructure.d.ts +8 -0
- package/dist/handlers/handleGetStructure.d.ts.map +1 -0
- package/dist/handlers/handleGetStructure.js +19 -0
- package/dist/handlers/handleGetTable.d.ts +8 -0
- package/dist/handlers/handleGetTable.d.ts.map +1 -0
- package/dist/handlers/handleGetTable.js +19 -0
- package/dist/handlers/handleGetTableContents.d.ts +8 -0
- package/dist/handlers/handleGetTableContents.d.ts.map +1 -0
- package/dist/handlers/handleGetTableContents.js +22 -0
- package/dist/handlers/handleGetTransaction.d.ts +8 -0
- package/dist/handlers/handleGetTransaction.d.ts.map +1 -0
- package/dist/handlers/handleGetTransaction.js +19 -0
- package/dist/handlers/handleGetTypeInfo.d.ts +8 -0
- package/dist/handlers/handleGetTypeInfo.d.ts.map +1 -0
- package/dist/handlers/handleGetTypeInfo.js +32 -0
- package/dist/handlers/handleSearchObject.d.ts +8 -0
- package/dist/handlers/handleSearchObject.d.ts.map +1 -0
- package/dist/handlers/handleSearchObject.js +20 -0
- package/dist/handlers/handleZObjectQuery.d.ts +8 -0
- package/dist/handlers/handleZObjectQuery.d.ts.map +1 -0
- package/dist/handlers/handleZObjectQuery.js +25 -0
- package/dist/handlers/handleZTableQuery.d.ts +8 -0
- package/dist/handlers/handleZTableQuery.d.ts.map +1 -0
- package/dist/handlers/handleZTableQuery.js +20 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/lib/download.d.ts +49 -0
- package/dist/lib/download.d.ts.map +1 -0
- package/dist/lib/download.js +78 -0
- package/dist/lib/index.d.ts +9 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +23 -0
- package/dist/lib/utils.d.ts +31 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +272 -0
- package/dist/producer/RecursiveQueryProducer.d.ts +92 -0
- package/dist/producer/RecursiveQueryProducer.d.ts.map +1 -0
- package/dist/producer/RecursiveQueryProducer.js +496 -0
- package/dist/producer/handlers/handleZObjectQuery.d.ts +8 -0
- package/dist/producer/handlers/handleZObjectQuery.d.ts.map +1 -0
- package/dist/producer/handlers/handleZObjectQuery.js +24 -0
- package/dist/producer/handlers/index.d.ts +6 -0
- package/dist/producer/handlers/index.d.ts.map +1 -0
- package/dist/producer/handlers/index.js +21 -0
- package/dist/producer/index.d.ts +7 -0
- package/dist/producer/index.d.ts.map +1 -0
- package/dist/producer/index.js +22 -0
- package/dist/recursive/abapStructures.d.ts +377 -0
- package/dist/recursive/abapStructures.d.ts.map +1 -0
- package/dist/recursive/abapStructures.js +2 -0
- package/dist/recursive/generate/baseGenerator.d.ts +34 -0
- package/dist/recursive/generate/baseGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/baseGenerator.js +112 -0
- package/dist/recursive/generate/domainGenerator.d.ts +26 -0
- package/dist/recursive/generate/domainGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/domainGenerator.js +128 -0
- package/dist/recursive/generate/functionGroupGenerator.d.ts +30 -0
- package/dist/recursive/generate/functionGroupGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/functionGroupGenerator.js +90 -0
- package/dist/recursive/generate/index.d.ts +12 -0
- package/dist/recursive/generate/index.d.ts.map +1 -0
- package/dist/recursive/generate/index.js +34 -0
- package/dist/recursive/generate/messageGenerator.d.ts +16 -0
- package/dist/recursive/generate/messageGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/messageGenerator.js +73 -0
- package/dist/recursive/generate/screenGenerator.d.ts +173 -0
- package/dist/recursive/generate/screenGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/screenGenerator.js +858 -0
- package/dist/recursive/generate/structureGenerator.d.ts +22 -0
- package/dist/recursive/generate/structureGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/structureGenerator.js +88 -0
- package/dist/recursive/generate/textElementGenerator.d.ts +16 -0
- package/dist/recursive/generate/textElementGenerator.d.ts.map +1 -0
- package/dist/recursive/generate/textElementGenerator.js +68 -0
- package/dist/recursive/handleRecursiveObjectQuery.d.ts +94 -0
- package/dist/recursive/handleRecursiveObjectQuery.d.ts.map +1 -0
- package/dist/recursive/handleRecursiveObjectQuery.js +219 -0
- package/dist/recursive/recursiveObjectQuery.d.ts +159 -0
- package/dist/recursive/recursiveObjectQuery.d.ts.map +1 -0
- package/dist/recursive/recursiveObjectQuery.js +1358 -0
- package/dist/recursive/recursiveQueryConfig.d.ts +129 -0
- package/dist/recursive/recursiveQueryConfig.d.ts.map +1 -0
- package/dist/recursive/recursiveQueryConfig.js +133 -0
- package/dist/recursive/tableStructures.d.ts +196 -0
- package/dist/recursive/tableStructures.d.ts.map +1 -0
- package/dist/recursive/tableStructures.js +10 -0
- package/package.json +47 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis 队列管理器
|
|
4
|
+
* 负责管理任务队列、Job状态、缓存等
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.RedisQueueManager = void 0;
|
|
11
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
12
|
+
const util_1 = require("util");
|
|
13
|
+
const zlib_1 = require("zlib");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
const config_1 = require("./config");
|
|
16
|
+
const gzipAsync = (0, util_1.promisify)(zlib_1.gzip);
|
|
17
|
+
const gunzipAsync = (0, util_1.promisify)(zlib_1.gunzip);
|
|
18
|
+
/**
|
|
19
|
+
* Redis 队列管理器
|
|
20
|
+
* ✅ 优化:改进连接复用机制,减少连接数
|
|
21
|
+
*/
|
|
22
|
+
class RedisQueueManager {
|
|
23
|
+
redis;
|
|
24
|
+
enableCompression;
|
|
25
|
+
pubClient; // ✅ Pub/Sub 发布客户端
|
|
26
|
+
subClients = new Map(); // ✅ 订阅客户端映射
|
|
27
|
+
sharedSubClient; // ✅ 优化:共享订阅客户端(用于多个 Job)
|
|
28
|
+
constructor(redis) {
|
|
29
|
+
this.redis = redis || new ioredis_1.default(config_1.redisConfig);
|
|
30
|
+
this.pubClient = this.redis; // 使用主连接作为发布客户端
|
|
31
|
+
this.enableCompression = config_1.queueConfig.enableCompression;
|
|
32
|
+
// 监听连接事件
|
|
33
|
+
this.redis.on('connect', () => {
|
|
34
|
+
console.error('[Redis] 已连接到 Redis 服务器');
|
|
35
|
+
});
|
|
36
|
+
this.redis.on('error', (err) => {
|
|
37
|
+
console.error('[Redis] 连接错误:', err);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// ============================================
|
|
41
|
+
// Job 管理
|
|
42
|
+
// ============================================
|
|
43
|
+
/**
|
|
44
|
+
* 创建新的 Job
|
|
45
|
+
*/
|
|
46
|
+
async createJob(jobId, rootObjectType, rootObjectName, config) {
|
|
47
|
+
const job = {
|
|
48
|
+
jobId,
|
|
49
|
+
rootObjectType,
|
|
50
|
+
rootObjectName,
|
|
51
|
+
config,
|
|
52
|
+
status: 'PENDING',
|
|
53
|
+
totalTasks: 0,
|
|
54
|
+
completedTasks: 0,
|
|
55
|
+
failedTasks: 0,
|
|
56
|
+
skippedTasks: 0,
|
|
57
|
+
createdAt: new Date(),
|
|
58
|
+
errors: []
|
|
59
|
+
};
|
|
60
|
+
const jobKey = types_1.RedisKeys.JOB(jobId);
|
|
61
|
+
await this.redis.hmset(jobKey, this.serializeJob(job));
|
|
62
|
+
// 设置过期时间(1小时)
|
|
63
|
+
await this.redis.expire(jobKey, config_1.queueConfig.jobTimeout / 1000);
|
|
64
|
+
// 添加到Job索引
|
|
65
|
+
await this.redis.zadd(types_1.RedisKeys.JOB_INDEX, Date.now(), jobId);
|
|
66
|
+
console.error(`[Job] 创建 Job: ${jobId}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 获取 Job 信息
|
|
70
|
+
*/
|
|
71
|
+
async getJob(jobId) {
|
|
72
|
+
const jobKey = types_1.RedisKeys.JOB(jobId);
|
|
73
|
+
const jobData = await this.redis.hgetall(jobKey);
|
|
74
|
+
if (!jobData || Object.keys(jobData).length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return this.deserializeJob(jobData);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 更新 Job 状态
|
|
81
|
+
*/
|
|
82
|
+
async updateJobStatus(jobId, status) {
|
|
83
|
+
const jobKey = types_1.RedisKeys.JOB(jobId);
|
|
84
|
+
await this.redis.hset(jobKey, 'status', status);
|
|
85
|
+
if (status === 'RUNNING') {
|
|
86
|
+
await this.redis.hset(jobKey, 'startedAt', new Date().toISOString());
|
|
87
|
+
}
|
|
88
|
+
else if (status === 'COMPLETED' || status === 'FAILED' || status === 'CANCELLED') {
|
|
89
|
+
await this.redis.hset(jobKey, 'completedAt', new Date().toISOString());
|
|
90
|
+
}
|
|
91
|
+
console.error(`[Job] 更新 Job 状态: ${jobId} -> ${status}`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 增加 Job 的任务计数
|
|
95
|
+
*/
|
|
96
|
+
async incrementJobTaskCount(jobId, field) {
|
|
97
|
+
const jobKey = types_1.RedisKeys.JOB(jobId);
|
|
98
|
+
await this.redis.hincrby(jobKey, field, 1);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 记录 Job 错误
|
|
102
|
+
* @param jobId Job ID
|
|
103
|
+
* @param error 错误信息
|
|
104
|
+
*/
|
|
105
|
+
async recordJobError(jobId, error) {
|
|
106
|
+
const jobKey = types_1.RedisKeys.JOB(jobId);
|
|
107
|
+
await this.redis.rpush(`${jobKey}:errors`, error);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 获取活跃的 Job IDs
|
|
111
|
+
*/
|
|
112
|
+
async getActiveJobIds() {
|
|
113
|
+
const jobIds = await this.redis.zrange(types_1.RedisKeys.JOB_INDEX, 0, -1);
|
|
114
|
+
const activeJobs = [];
|
|
115
|
+
for (const jobId of jobIds) {
|
|
116
|
+
const job = await this.getJob(jobId);
|
|
117
|
+
if (job && (job.status === 'PENDING' || job.status === 'RUNNING')) {
|
|
118
|
+
activeJobs.push(jobId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return activeJobs;
|
|
122
|
+
}
|
|
123
|
+
// ============================================
|
|
124
|
+
// Task 管理
|
|
125
|
+
// ============================================
|
|
126
|
+
/**
|
|
127
|
+
* 创建任务并推送到Job专属队列
|
|
128
|
+
*/
|
|
129
|
+
async createTask(task, isRetry = false) {
|
|
130
|
+
const queueKey = types_1.RedisKeys.TASK_QUEUE(task.jobId); // ⚠️ Job专属队列
|
|
131
|
+
const taskKey = types_1.RedisKeys.TASK(task.taskId);
|
|
132
|
+
// 1. 保存任务详情
|
|
133
|
+
await this.redis.hmset(taskKey, this.serializeTask(task));
|
|
134
|
+
// 设置过期时间
|
|
135
|
+
await this.redis.expire(taskKey, config_1.queueConfig.cacheExpiry);
|
|
136
|
+
// 2. 推送到Job专属队列(优先级队列)
|
|
137
|
+
await this.redis.zadd(queueKey, task.priority, task.taskId);
|
|
138
|
+
// 3. 增加队列大小计数
|
|
139
|
+
await this.redis.incr(types_1.RedisKeys.QUEUE_SIZE(task.jobId));
|
|
140
|
+
// 4. 添加到Job的任务列表
|
|
141
|
+
await this.redis.sadd(types_1.RedisKeys.JOB_TASKS(task.jobId), task.taskId);
|
|
142
|
+
// 5. 增加Job的总任务数(⚠️ 重试任务不增加总数,避免进度显示错误)
|
|
143
|
+
if (!isRetry) {
|
|
144
|
+
await this.incrementJobTaskCount(task.jobId, 'totalTasks');
|
|
145
|
+
}
|
|
146
|
+
return task.taskId;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 从指定Job的队列拉取任务
|
|
150
|
+
*/
|
|
151
|
+
async pullTask(jobId, timeout = 5) {
|
|
152
|
+
const queueKey = types_1.RedisKeys.TASK_QUEUE(jobId); // ⚠️ 从指定Job队列拉取
|
|
153
|
+
// 阻塞式拉取(优先级最高的任务)
|
|
154
|
+
const result = await this.redis.bzpopmin(queueKey, timeout);
|
|
155
|
+
if (!result || result.length < 2) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const taskId = result[1];
|
|
159
|
+
const task = await this.getTask(taskId);
|
|
160
|
+
if (!task) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// 减少队列大小计数
|
|
164
|
+
await this.redis.decr(types_1.RedisKeys.QUEUE_SIZE(jobId));
|
|
165
|
+
return task;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 从多个Job的队列拉取任务(消费者可以处理多个Job)
|
|
169
|
+
*/
|
|
170
|
+
async pullTaskFromAnyJob(jobIds, timeout = 5) {
|
|
171
|
+
if (jobIds.length === 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const queueKeys = jobIds.map(id => types_1.RedisKeys.TASK_QUEUE(id));
|
|
175
|
+
// 阻塞式拉取(从多个队列中取优先级最高的)
|
|
176
|
+
const result = await this.redis.bzpopmin(...queueKeys, timeout);
|
|
177
|
+
if (!result || result.length < 2) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const taskId = result[1];
|
|
181
|
+
const task = await this.getTask(taskId);
|
|
182
|
+
// 减少队列大小计数
|
|
183
|
+
if (task) {
|
|
184
|
+
await this.redis.decr(types_1.RedisKeys.QUEUE_SIZE(task.jobId));
|
|
185
|
+
}
|
|
186
|
+
return task;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 获取任务信息
|
|
190
|
+
*/
|
|
191
|
+
async getTask(taskId) {
|
|
192
|
+
const taskKey = types_1.RedisKeys.TASK(taskId);
|
|
193
|
+
const taskData = await this.redis.hgetall(taskKey);
|
|
194
|
+
if (!taskData || Object.keys(taskData).length === 0) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return this.deserializeTask(taskData);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 更新任务状态
|
|
201
|
+
*/
|
|
202
|
+
async updateTaskStatus(taskId, status, workerId, error) {
|
|
203
|
+
const taskKey = types_1.RedisKeys.TASK(taskId);
|
|
204
|
+
const updates = { status };
|
|
205
|
+
if (status === 'RUNNING') {
|
|
206
|
+
updates.startedAt = new Date().toISOString();
|
|
207
|
+
if (workerId) {
|
|
208
|
+
updates.workerId = workerId;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (status === 'COMPLETED' || status === 'FAILED' || status === 'SKIPPED') {
|
|
212
|
+
updates.completedAt = new Date().toISOString();
|
|
213
|
+
}
|
|
214
|
+
if (error) {
|
|
215
|
+
updates.error = error;
|
|
216
|
+
}
|
|
217
|
+
await this.redis.hmset(taskKey, updates);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 完成任务(✅ 优化:发送队列空间通知)
|
|
221
|
+
*/
|
|
222
|
+
async completeTask(taskId, result) {
|
|
223
|
+
const task = await this.getTask(taskId);
|
|
224
|
+
if (!task) {
|
|
225
|
+
console.error(`[Task] 任务不存在: ${taskId}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await this.updateTaskStatus(taskId, 'COMPLETED');
|
|
229
|
+
await this.incrementJobTaskCount(task.jobId, 'completedTasks');
|
|
230
|
+
// ✅ 通知队列有空间(事件驱动的背压控制)
|
|
231
|
+
await this.notifyQueueSpace(task.jobId);
|
|
232
|
+
// 检查Job是否完成
|
|
233
|
+
await this.checkJobCompletion(task.jobId);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 任务失败
|
|
237
|
+
*/
|
|
238
|
+
async failTask(taskId, error) {
|
|
239
|
+
const task = await this.getTask(taskId);
|
|
240
|
+
if (!task) {
|
|
241
|
+
console.error(`[Task] 任务不存在: ${taskId}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// 检查是否需要重试
|
|
245
|
+
if (task.retryCount < task.maxRetries) {
|
|
246
|
+
// 重新入队
|
|
247
|
+
const retryTask = {
|
|
248
|
+
...task,
|
|
249
|
+
retryCount: task.retryCount + 1,
|
|
250
|
+
status: 'PENDING',
|
|
251
|
+
priority: task.priority + 10000 // 降低优先级
|
|
252
|
+
};
|
|
253
|
+
// ⚠️ 重要:传入 isRetry = true,避免重复计算 totalTasks
|
|
254
|
+
await this.createTask(retryTask, true);
|
|
255
|
+
console.error(`[Task] 任务重试: ${taskId} (${retryTask.retryCount}/${task.maxRetries})`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// 标记为失败
|
|
259
|
+
await this.updateTaskStatus(taskId, 'FAILED', undefined, error);
|
|
260
|
+
await this.incrementJobTaskCount(task.jobId, 'failedTasks');
|
|
261
|
+
// 删除失败任务的缓存,释放内存
|
|
262
|
+
await this.deleteCache(task.jobId, task.objectKey);
|
|
263
|
+
console.error(`[Task] 删除失败任务的缓存: ${task.objectKey}`);
|
|
264
|
+
// 记录错误到Job
|
|
265
|
+
await this.recordJobError(task.jobId, `任务失败 (${task.objectKey}): ${error}`);
|
|
266
|
+
// 检查Job是否完成
|
|
267
|
+
await this.checkJobCompletion(task.jobId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 检查Job是否完成(✅ 修复:只有在所有任务处理完后才更新状态)
|
|
272
|
+
*/
|
|
273
|
+
async checkJobCompletion(jobId) {
|
|
274
|
+
const job = await this.getJob(jobId);
|
|
275
|
+
if (!job) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const finishedTasks = job.completedTasks + job.failedTasks + job.skippedTasks;
|
|
279
|
+
// ✅ 修复:只有当所有任务都处理完后才更新 Job 状态
|
|
280
|
+
// 之前的问题:只要有失败任务就立即标记为 FAILED,导致 Producer 提前退出
|
|
281
|
+
if (finishedTasks >= job.totalTasks && job.totalTasks > 0) {
|
|
282
|
+
// 计算失败率
|
|
283
|
+
const failureRate = job.failedTasks / job.totalTasks;
|
|
284
|
+
// 根据失败率决定状态
|
|
285
|
+
// - 如果没有失败任务:COMPLETED
|
|
286
|
+
// - 如果失败率 < 50%:COMPLETED(部分失败也算完成)
|
|
287
|
+
// - 如果失败率 >= 50%:FAILED
|
|
288
|
+
let status;
|
|
289
|
+
if (job.failedTasks === 0) {
|
|
290
|
+
status = 'COMPLETED';
|
|
291
|
+
}
|
|
292
|
+
else if (failureRate < 0.5) {
|
|
293
|
+
status = 'COMPLETED'; // 部分失败,但大部分成功
|
|
294
|
+
console.error(`[Job] Job ${jobId} 完成(部分失败: ${job.failedTasks}/${job.totalTasks})`);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
status = 'FAILED'; // 失败率过高
|
|
298
|
+
}
|
|
299
|
+
await this.updateJobStatus(jobId, status);
|
|
300
|
+
console.error(`[Job] Job完成: ${jobId}, 状态: ${status}, 成功: ${job.completedTasks}, 失败: ${job.failedTasks}, 跳过: ${job.skippedTasks}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// ============================================
|
|
304
|
+
// 队列监控
|
|
305
|
+
// ============================================
|
|
306
|
+
/**
|
|
307
|
+
* 获取指定Job的队列大小
|
|
308
|
+
*/
|
|
309
|
+
async getQueueSize(jobId) {
|
|
310
|
+
const sizeStr = await this.redis.get(types_1.RedisKeys.QUEUE_SIZE(jobId));
|
|
311
|
+
return parseInt(sizeStr || '0');
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 获取队列中的任务数(精确但较慢)
|
|
315
|
+
*/
|
|
316
|
+
async getQueueTaskCount(jobId) {
|
|
317
|
+
const queueKey = types_1.RedisKeys.TASK_QUEUE(jobId);
|
|
318
|
+
return await this.redis.zcard(queueKey);
|
|
319
|
+
}
|
|
320
|
+
// ============================================
|
|
321
|
+
// 缓存管理
|
|
322
|
+
// ============================================
|
|
323
|
+
/**
|
|
324
|
+
* 缓存对象数据
|
|
325
|
+
*/
|
|
326
|
+
async cacheObject(jobId, objectKey, data) {
|
|
327
|
+
const cacheKey = types_1.RedisKeys.CACHE(jobId, objectKey);
|
|
328
|
+
let cachedData;
|
|
329
|
+
if (this.enableCompression) {
|
|
330
|
+
// 压缩存储
|
|
331
|
+
const jsonString = JSON.stringify(data);
|
|
332
|
+
const compressed = await gzipAsync(Buffer.from(jsonString));
|
|
333
|
+
cachedData = compressed.toString('base64');
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
cachedData = JSON.stringify(data);
|
|
337
|
+
}
|
|
338
|
+
await this.redis.setex(cacheKey, config_1.queueConfig.cacheExpiry, cachedData);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 获取缓存的对象数据
|
|
342
|
+
*/
|
|
343
|
+
async getCache(jobId, objectKey) {
|
|
344
|
+
const cacheKey = types_1.RedisKeys.CACHE(jobId, objectKey);
|
|
345
|
+
const cachedData = await this.redis.get(cacheKey);
|
|
346
|
+
if (!cachedData) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
if (this.enableCompression) {
|
|
350
|
+
// 解压缩
|
|
351
|
+
const buffer = Buffer.from(cachedData, 'base64');
|
|
352
|
+
const decompressed = await gunzipAsync(buffer);
|
|
353
|
+
return JSON.parse(decompressed.toString());
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
return JSON.parse(cachedData);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* 删除缓存
|
|
361
|
+
*/
|
|
362
|
+
async deleteCache(jobId, objectKey) {
|
|
363
|
+
const cacheKey = types_1.RedisKeys.CACHE(jobId, objectKey);
|
|
364
|
+
await this.redis.del(cacheKey);
|
|
365
|
+
}
|
|
366
|
+
// ============================================
|
|
367
|
+
// 去重检查
|
|
368
|
+
// ============================================
|
|
369
|
+
/**
|
|
370
|
+
* 检查对象是否已访问
|
|
371
|
+
*/
|
|
372
|
+
async isVisited(jobId, objectKey) {
|
|
373
|
+
const visitedKey = types_1.RedisKeys.VISITED(jobId);
|
|
374
|
+
return (await this.redis.sismember(visitedKey, objectKey)) === 1;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* 标记对象为已访问
|
|
378
|
+
*/
|
|
379
|
+
async markVisited(jobId, objectKey) {
|
|
380
|
+
const visitedKey = types_1.RedisKeys.VISITED(jobId);
|
|
381
|
+
await this.redis.sadd(visitedKey, objectKey);
|
|
382
|
+
await this.redis.expire(visitedKey, config_1.queueConfig.cacheExpiry);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 原子性地标记对象为已访问(如果未访问则标记)
|
|
386
|
+
* @param jobId Job ID
|
|
387
|
+
* @param objectKey 对象键
|
|
388
|
+
* @returns true 表示首次访问(未被访问过),false 表示已访问
|
|
389
|
+
*/
|
|
390
|
+
async markVisitedIfNotExists(jobId, objectKey) {
|
|
391
|
+
const visitedKey = types_1.RedisKeys.VISITED(jobId);
|
|
392
|
+
// SADD 返回 1 表示新增成功(未访问),返回 0 表示已存在(已访问)
|
|
393
|
+
const result = await this.redis.sadd(visitedKey, objectKey);
|
|
394
|
+
// 设置过期时间
|
|
395
|
+
await this.redis.expire(visitedKey, config_1.queueConfig.cacheExpiry);
|
|
396
|
+
return result === 1; // true = 首次访问,false = 已访问
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* 检查附加数据是否已处理(用于去重)
|
|
400
|
+
* @param jobId Job ID
|
|
401
|
+
* @param category 数据类别(textelements/messages/guititle/guistatus/screens)
|
|
402
|
+
* @param programName 程序名称
|
|
403
|
+
* @returns true 表示已处理,false 表示未处理
|
|
404
|
+
*/
|
|
405
|
+
async hasProcessed(jobId, category, programName) {
|
|
406
|
+
const key = `abap:job:${jobId}:processed:${category}:${programName}`;
|
|
407
|
+
return (await this.redis.exists(key)) === 1;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 标记附加数据为已处理
|
|
411
|
+
* @param jobId Job ID
|
|
412
|
+
* @param category 数据类别
|
|
413
|
+
* @param programName 程序名称
|
|
414
|
+
*/
|
|
415
|
+
async markProcessed(jobId, category, programName) {
|
|
416
|
+
const key = `abap:job:${jobId}:processed:${category}:${programName}`;
|
|
417
|
+
await this.redis.setex(key, config_1.queueConfig.cacheExpiry, '1');
|
|
418
|
+
}
|
|
419
|
+
// ============================================
|
|
420
|
+
// 进度追踪
|
|
421
|
+
// ============================================
|
|
422
|
+
/**
|
|
423
|
+
* 更新进度
|
|
424
|
+
*/
|
|
425
|
+
async updateProgress(jobId, progress) {
|
|
426
|
+
const progressKey = types_1.RedisKeys.PROGRESS(jobId);
|
|
427
|
+
await this.redis.hmset(progressKey, {
|
|
428
|
+
totalTasks: progress.totalTasks.toString(),
|
|
429
|
+
completedTasks: progress.completedTasks.toString(),
|
|
430
|
+
failedTasks: progress.failedTasks.toString(),
|
|
431
|
+
skippedTasks: progress.skippedTasks.toString(),
|
|
432
|
+
percentage: progress.percentage.toFixed(2),
|
|
433
|
+
lastUpdateTime: progress.lastUpdateTime.toISOString()
|
|
434
|
+
});
|
|
435
|
+
await this.redis.expire(progressKey, config_1.queueConfig.cacheExpiry);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 获取进度
|
|
439
|
+
*/
|
|
440
|
+
async getProgress(jobId) {
|
|
441
|
+
const progressKey = types_1.RedisKeys.PROGRESS(jobId);
|
|
442
|
+
const progressData = await this.redis.hgetall(progressKey);
|
|
443
|
+
if (!progressData || Object.keys(progressData).length === 0) {
|
|
444
|
+
// 从Job数据中获取进度
|
|
445
|
+
const job = await this.getJob(jobId);
|
|
446
|
+
if (!job) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const total = job.totalTasks || 1;
|
|
450
|
+
const completed = job.completedTasks + job.failedTasks + job.skippedTasks;
|
|
451
|
+
return {
|
|
452
|
+
totalTasks: job.totalTasks,
|
|
453
|
+
completedTasks: job.completedTasks,
|
|
454
|
+
failedTasks: job.failedTasks,
|
|
455
|
+
skippedTasks: job.skippedTasks,
|
|
456
|
+
percentage: (completed / total) * 100,
|
|
457
|
+
lastUpdateTime: new Date()
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
totalTasks: parseInt(progressData.totalTasks || '0'),
|
|
462
|
+
completedTasks: parseInt(progressData.completedTasks || '0'),
|
|
463
|
+
failedTasks: parseInt(progressData.failedTasks || '0'),
|
|
464
|
+
skippedTasks: parseInt(progressData.skippedTasks || '0'),
|
|
465
|
+
percentage: parseFloat(progressData.percentage || '0'),
|
|
466
|
+
lastUpdateTime: new Date(progressData.lastUpdateTime)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
// ============================================
|
|
470
|
+
// 辅助方法
|
|
471
|
+
// ============================================
|
|
472
|
+
/**
|
|
473
|
+
* 序列化 Job
|
|
474
|
+
*/
|
|
475
|
+
serializeJob(job) {
|
|
476
|
+
return {
|
|
477
|
+
jobId: job.jobId,
|
|
478
|
+
rootObjectType: job.rootObjectType,
|
|
479
|
+
rootObjectName: job.rootObjectName,
|
|
480
|
+
config: JSON.stringify(job.config),
|
|
481
|
+
status: job.status,
|
|
482
|
+
totalTasks: job.totalTasks.toString(),
|
|
483
|
+
completedTasks: job.completedTasks.toString(),
|
|
484
|
+
failedTasks: job.failedTasks.toString(),
|
|
485
|
+
skippedTasks: job.skippedTasks.toString(),
|
|
486
|
+
createdAt: job.createdAt.toISOString(),
|
|
487
|
+
startedAt: job.startedAt?.toISOString() || '',
|
|
488
|
+
completedAt: job.completedAt?.toISOString() || '',
|
|
489
|
+
errors: JSON.stringify(job.errors)
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* 反序列化 Job
|
|
494
|
+
*/
|
|
495
|
+
deserializeJob(data) {
|
|
496
|
+
return {
|
|
497
|
+
jobId: data.jobId,
|
|
498
|
+
rootObjectType: data.rootObjectType,
|
|
499
|
+
rootObjectName: data.rootObjectName,
|
|
500
|
+
config: JSON.parse(data.config || '{}'),
|
|
501
|
+
status: data.status,
|
|
502
|
+
totalTasks: parseInt(data.totalTasks || '0'),
|
|
503
|
+
completedTasks: parseInt(data.completedTasks || '0'),
|
|
504
|
+
failedTasks: parseInt(data.failedTasks || '0'),
|
|
505
|
+
skippedTasks: parseInt(data.skippedTasks || '0'),
|
|
506
|
+
createdAt: new Date(data.createdAt),
|
|
507
|
+
startedAt: data.startedAt ? new Date(data.startedAt) : undefined,
|
|
508
|
+
completedAt: data.completedAt ? new Date(data.completedAt) : undefined,
|
|
509
|
+
errors: JSON.parse(data.errors || '[]')
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* 序列化 Task
|
|
514
|
+
*/
|
|
515
|
+
serializeTask(task) {
|
|
516
|
+
return {
|
|
517
|
+
taskId: task.taskId,
|
|
518
|
+
jobId: task.jobId,
|
|
519
|
+
objectKey: task.objectKey,
|
|
520
|
+
taskType: task.taskType,
|
|
521
|
+
objectType: task.objectType,
|
|
522
|
+
objectName: task.objectName,
|
|
523
|
+
depth: task.depth.toString(),
|
|
524
|
+
priority: task.priority.toString(),
|
|
525
|
+
status: task.status,
|
|
526
|
+
retryCount: task.retryCount.toString(),
|
|
527
|
+
maxRetries: task.maxRetries.toString(),
|
|
528
|
+
createdAt: task.createdAt.toISOString(),
|
|
529
|
+
startedAt: task.startedAt?.toISOString() || '',
|
|
530
|
+
completedAt: task.completedAt?.toISOString() || '',
|
|
531
|
+
metadata: JSON.stringify(task.metadata),
|
|
532
|
+
error: task.error || '',
|
|
533
|
+
workerId: task.workerId || ''
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* 反序列化 Task
|
|
538
|
+
*/
|
|
539
|
+
deserializeTask(data) {
|
|
540
|
+
return {
|
|
541
|
+
taskId: data.taskId,
|
|
542
|
+
jobId: data.jobId,
|
|
543
|
+
objectKey: data.objectKey,
|
|
544
|
+
taskType: data.taskType,
|
|
545
|
+
objectType: data.objectType,
|
|
546
|
+
objectName: data.objectName,
|
|
547
|
+
depth: parseInt(data.depth || '0'),
|
|
548
|
+
priority: parseInt(data.priority || '0'),
|
|
549
|
+
status: data.status,
|
|
550
|
+
retryCount: parseInt(data.retryCount || '0'),
|
|
551
|
+
maxRetries: parseInt(data.maxRetries || '3'),
|
|
552
|
+
createdAt: new Date(data.createdAt),
|
|
553
|
+
startedAt: data.startedAt ? new Date(data.startedAt) : undefined,
|
|
554
|
+
completedAt: data.completedAt ? new Date(data.completedAt) : undefined,
|
|
555
|
+
metadata: JSON.parse(data.metadata || '{}'),
|
|
556
|
+
error: data.error || undefined,
|
|
557
|
+
workerId: data.workerId || undefined
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
// ============================================
|
|
561
|
+
// Pub/Sub 通知机制(✅ 优化:事件驱动的背压控制)
|
|
562
|
+
// ============================================
|
|
563
|
+
/**
|
|
564
|
+
* 发布队列空间可用事件(在 Consumer 完成任务时调用)
|
|
565
|
+
*/
|
|
566
|
+
async notifyQueueSpace(jobId) {
|
|
567
|
+
const channel = `abap:queue:space:${jobId}`;
|
|
568
|
+
await this.pubClient.publish(channel, Date.now().toString());
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 订阅队列空间可用事件
|
|
572
|
+
* ✅ 优化:使用共享订阅客户端,减少连接数
|
|
573
|
+
* @returns 返回取消订阅的函数
|
|
574
|
+
*/
|
|
575
|
+
async subscribeQueueSpace(jobId, callback) {
|
|
576
|
+
const channel = `abap:queue:space:${jobId}`;
|
|
577
|
+
// ✅ 优化:复用共享订阅客户端,而非每次创建新连接
|
|
578
|
+
if (!this.sharedSubClient) {
|
|
579
|
+
this.sharedSubClient = this.redis.duplicate();
|
|
580
|
+
// 监听所有消息并分发给相应的回调
|
|
581
|
+
this.sharedSubClient.on('message', (ch, message) => {
|
|
582
|
+
// 从 subClients 获取对应的回调列表
|
|
583
|
+
const callbacks = this.subClients.get(ch);
|
|
584
|
+
if (callbacks && Array.isArray(callbacks)) {
|
|
585
|
+
callbacks.forEach((cb) => {
|
|
586
|
+
if (typeof cb === 'function') {
|
|
587
|
+
cb();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
console.error('[Redis] 创建共享订阅客户端');
|
|
593
|
+
}
|
|
594
|
+
// 获取或创建该频道的回调列表
|
|
595
|
+
if (!this.subClients.has(channel)) {
|
|
596
|
+
this.subClients.set(channel, []);
|
|
597
|
+
// 首次订阅该频道
|
|
598
|
+
await this.sharedSubClient.subscribe(channel);
|
|
599
|
+
}
|
|
600
|
+
// 添加回调到列表
|
|
601
|
+
const callbacks = this.subClients.get(channel);
|
|
602
|
+
callbacks.push(callback);
|
|
603
|
+
// 返回取消订阅函数
|
|
604
|
+
return async () => {
|
|
605
|
+
const callbacks = this.subClients.get(channel);
|
|
606
|
+
if (callbacks) {
|
|
607
|
+
// 移除该回调
|
|
608
|
+
const index = callbacks.indexOf(callback);
|
|
609
|
+
if (index !== -1) {
|
|
610
|
+
callbacks.splice(index, 1);
|
|
611
|
+
}
|
|
612
|
+
// 如果该频道没有回调了,取消订阅
|
|
613
|
+
if (callbacks.length === 0) {
|
|
614
|
+
this.subClients.delete(channel);
|
|
615
|
+
if (this.sharedSubClient) {
|
|
616
|
+
await this.sharedSubClient.unsubscribe(channel);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
// ============================================
|
|
623
|
+
// 资源管理
|
|
624
|
+
// ============================================
|
|
625
|
+
/**
|
|
626
|
+
* 关闭连接
|
|
627
|
+
* ✅ 优化:正确关闭共享订阅客户端
|
|
628
|
+
*/
|
|
629
|
+
async close() {
|
|
630
|
+
// 关闭共享订阅客户端
|
|
631
|
+
if (this.sharedSubClient) {
|
|
632
|
+
try {
|
|
633
|
+
await this.sharedSubClient.quit();
|
|
634
|
+
console.error('[Redis] 已关闭共享订阅客户端');
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
console.error('[Redis] 关闭共享订阅客户端失败:', error);
|
|
638
|
+
}
|
|
639
|
+
this.sharedSubClient = undefined;
|
|
640
|
+
}
|
|
641
|
+
this.subClients.clear();
|
|
642
|
+
// 关闭主连接
|
|
643
|
+
await this.redis.quit();
|
|
644
|
+
console.error('[Redis] 已断开连接');
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* 清理Job的所有数据
|
|
648
|
+
*/
|
|
649
|
+
async cleanupJob(jobId) {
|
|
650
|
+
const keys = [
|
|
651
|
+
types_1.RedisKeys.JOB(jobId),
|
|
652
|
+
types_1.RedisKeys.JOB_TASKS(jobId),
|
|
653
|
+
types_1.RedisKeys.TASK_QUEUE(jobId),
|
|
654
|
+
types_1.RedisKeys.VISITED(jobId),
|
|
655
|
+
types_1.RedisKeys.PROGRESS(jobId),
|
|
656
|
+
types_1.RedisKeys.QUEUE_SIZE(jobId)
|
|
657
|
+
];
|
|
658
|
+
await this.redis.del(...keys);
|
|
659
|
+
await this.redis.zrem(types_1.RedisKeys.JOB_INDEX, jobId);
|
|
660
|
+
console.error(`[Job] 清理 Job 数据: ${jobId}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
exports.RedisQueueManager = RedisQueueManager;
|