amqplib-init 1.1.10 → 1.2.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/README.md +1 -0
- package/index.js +140 -129
- package/module/AutoReloader.js +139 -0
- package/module/ConfigResolver.js +83 -0
- package/module/ConnectionManager.js +181 -0
- package/module/MessageProcessor.js +145 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/index.js
CHANGED
|
@@ -1,142 +1,153 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
1
|
+
const ConfigResolver = require('./module/ConfigResolver');
|
|
2
|
+
const ConnectionManager = require('./module/ConnectionManager');
|
|
3
|
+
const MessageProcessor = require('./module/MessageProcessor');
|
|
4
|
+
const AutoReloader = require('./module/AutoReloader');
|
|
5
|
+
const log = require('chalk-style');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
/**
|
|
8
|
+
* AMQP初始化器主类
|
|
9
|
+
* 协调各个模块完成RabbitMQ的初始化和消息处理
|
|
10
|
+
*/
|
|
11
|
+
class AMQPInitializer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.configResolver = new ConfigResolver();
|
|
14
|
+
this.connectionManager = null;
|
|
15
|
+
this.messageProcessor = null;
|
|
16
|
+
this.autoReloader = null;
|
|
17
|
+
this.config = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 初始化AMQP连接和消息处理
|
|
22
|
+
* @param {Object} options - 配置选项
|
|
23
|
+
* @returns {Promise<void>}
|
|
24
|
+
*/
|
|
25
|
+
async init(options = {}) {
|
|
26
|
+
try {
|
|
27
|
+
// 1. 解析配置
|
|
28
|
+
this.config = this.configResolver.resolveConfig(options);
|
|
29
|
+
log.log('配置解析完成');
|
|
30
|
+
|
|
31
|
+
// 2. 解析连接地址
|
|
32
|
+
const amqpLink = await this.configResolver.resolveAmqpLink(
|
|
33
|
+
this.config.amqpAutoLink,
|
|
34
|
+
this.config.amqpLink
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// 3. 初始化各个模块
|
|
38
|
+
this.connectionManager = new ConnectionManager(this.config);
|
|
39
|
+
this.messageProcessor = new MessageProcessor(this.config);
|
|
40
|
+
this.autoReloader = new AutoReloader(this.config);
|
|
41
|
+
|
|
42
|
+
// 4. 设置连接地址并初始化连接
|
|
43
|
+
this.connectionManager.setAmqpLink(amqpLink);
|
|
44
|
+
const channel = await this.connectionManager.initialize();
|
|
45
|
+
|
|
46
|
+
// 5. 重新定义重连逻辑,确保重连后重新启动消息处理和自动重载
|
|
47
|
+
this._setupReconnectHandler();
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
// 6. 启动消息处理
|
|
50
|
+
await this.messageProcessor.startConsuming(channel);
|
|
51
|
+
log.log('消息处理器已启动');
|
|
52
|
+
|
|
53
|
+
// 7. 启动自动重载监控
|
|
54
|
+
this.autoReloader.start(channel);
|
|
55
|
+
|
|
56
|
+
// 8. 执行初始化完成回调
|
|
57
|
+
this.config.finish();
|
|
58
|
+
|
|
59
|
+
log.log('AMQP初始化完成');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
log.error('AMQP初始化失败:', error.message);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 设置重连处理器
|
|
68
|
+
* 确保重连后各个模块能够正确重新启动
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
_setupReconnectHandler() {
|
|
72
|
+
const originalReconnect = this.connectionManager.reconnect.bind(this.connectionManager);
|
|
73
|
+
|
|
74
|
+
this.connectionManager.reconnect = async () => {
|
|
32
75
|
try {
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
76
|
+
// 执行原始重连逻辑
|
|
77
|
+
const newChannel = await originalReconnect();
|
|
78
|
+
|
|
79
|
+
// 重连后重新启动消息处理
|
|
80
|
+
this.messageProcessor.clearProcessingMessages();
|
|
81
|
+
await this.messageProcessor.startConsuming(newChannel);
|
|
82
|
+
log.log('重连后消息处理器已重新启动');
|
|
83
|
+
|
|
84
|
+
// 重连后重新启动自动重载监控
|
|
85
|
+
this.autoReloader.restart(newChannel);
|
|
86
|
+
log.log('重连后自动重载监控已重新启动');
|
|
36
87
|
|
|
37
|
-
|
|
38
|
-
finalAmqpLink = `amqp://${info.AMQPLIB_USER}:${info.AMQPLIB_PWD}@${info.AMQPLIB_IP}:${info.AMQPLIB_PORT}`;
|
|
39
|
-
log.log(`✅ 自动获取连接信息成功: ${finalAmqpLink}`);
|
|
40
|
-
} else {
|
|
41
|
-
log.error('❌ 获取的连接信息不完整,使用默认连接地址');
|
|
42
|
-
}
|
|
88
|
+
return newChannel;
|
|
43
89
|
} catch (error) {
|
|
44
|
-
log.error('
|
|
45
|
-
|
|
90
|
+
log.error('重连处理失败:', error.message);
|
|
91
|
+
throw error;
|
|
46
92
|
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 重连函数
|
|
50
|
-
const reconnect = async () => {
|
|
51
|
-
log.error('连接丢失,正在尝试重连...');
|
|
52
|
-
await happy.sleep(2); // 重连前等待2秒
|
|
53
|
-
connection = await amqp.connect(finalAmqpLink, { heartbeat, timeout });
|
|
54
|
-
channel = await connection.createChannel();
|
|
55
|
-
await channel.assertQueue(channelName, { durable });
|
|
56
|
-
await channel.prefetch(prefetch);
|
|
57
|
-
await initHook({ channel, connection }); // 执行初始化钩子
|
|
58
|
-
log.log(`已重新连接到RabbitMQ,频道: ${channelName}`);
|
|
59
|
-
await consumeMessages(); // 开始消费消息
|
|
60
93
|
};
|
|
94
|
+
}
|
|
61
95
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
callback(content).then(() => {
|
|
73
|
-
const endTime = Date.now() - startTime;
|
|
74
|
-
log.log(`☘️ 消息处理完成,延迟: ${delay}ms,总时间: ${endTime}ms`);
|
|
75
|
-
setTimeout(() => channel.ack(msg), delay); // 延迟ack
|
|
76
|
-
}).catch(async (e) => {
|
|
77
|
-
// 拒绝消息并重新排队
|
|
78
|
-
log.log('‼️ 处理消息返回错误:', e);
|
|
79
|
-
await happy.sleep(5);
|
|
80
|
-
channel.reject(msg, true);
|
|
81
|
-
});
|
|
82
|
-
} catch (e) {
|
|
83
|
-
// 拒绝消息并重新排队
|
|
84
|
-
log.log('‼️ 处理消息时出错:', e.message);
|
|
85
|
-
await happy.sleep(5);
|
|
86
|
-
channel.reject(msg, true);
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
// 直接消息此消息;
|
|
90
|
-
channel.ack(msg);
|
|
91
|
-
log.error('收到无效消息(自动消费):', msg);
|
|
92
|
-
}
|
|
93
|
-
}, { noAck: false });
|
|
96
|
+
/**
|
|
97
|
+
* 获取当前状态信息
|
|
98
|
+
* @returns {Object} 状态信息
|
|
99
|
+
*/
|
|
100
|
+
getStatus() {
|
|
101
|
+
return {
|
|
102
|
+
isConnected: this.connectionManager ? this.connectionManager.isConnected() : false,
|
|
103
|
+
processingCount: this.messageProcessor ? this.messageProcessor.getProcessingCount() : 0,
|
|
104
|
+
autoReloaderActive: this.autoReloader ? this.autoReloader.isActive() : false,
|
|
105
|
+
channelName: this.config ? this.config.channelName : null
|
|
94
106
|
};
|
|
107
|
+
}
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
connection.on('error', (err) => {
|
|
108
|
-
log.error('连接错误:', err.message);
|
|
109
|
-
reconnect(); // 尝试重连
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// 连接关闭处理
|
|
113
|
-
connection.on('close', () => {
|
|
114
|
-
log.error('与RabbitMQ的连接已关闭');
|
|
115
|
-
reconnect(); // 尝试重连
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// 自动重载逻辑
|
|
119
|
-
if (autoReload > 0) {
|
|
120
|
-
setInterval(async () => {
|
|
121
|
-
try {
|
|
122
|
-
const { messageCount } = await channel.checkQueue(channelName);
|
|
123
|
-
const hookResult = await queryHook()
|
|
124
|
-
log.log(`MQ队列中有 ${messageCount} 条消息,检测是否可以重启: ${hookResult?1:0}`);
|
|
125
|
-
if (messageCount === 0 && hookResult) {
|
|
126
|
-
log.log('队列中没有消息,正在重载服务...');
|
|
127
|
-
shelljs?.exec(`pm2 reload ${pmId}`); // 使用PM2重载服务
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
log.error('检查队列长度时出错:', e);
|
|
131
|
-
}
|
|
132
|
-
}, autoReload);
|
|
133
|
-
}
|
|
134
|
-
finish(); // 执行初始化完成回调
|
|
135
|
-
} catch (e) {
|
|
136
|
-
log.error('初始化RabbitMQ连接时出错:', e);
|
|
109
|
+
/**
|
|
110
|
+
* 优雅关闭
|
|
111
|
+
* @returns {Promise<void>}
|
|
112
|
+
*/
|
|
113
|
+
async shutdown() {
|
|
114
|
+
try {
|
|
115
|
+
log.log('开始优雅关闭AMQP连接...');
|
|
116
|
+
|
|
117
|
+
// 停止自动重载监控
|
|
118
|
+
if (this.autoReloader) {
|
|
119
|
+
this.autoReloader.stop();
|
|
137
120
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
|
|
122
|
+
// 清理消息处理状态
|
|
123
|
+
if (this.messageProcessor) {
|
|
124
|
+
this.messageProcessor.clearProcessingMessages();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 关闭连接
|
|
128
|
+
if (this.connectionManager) {
|
|
129
|
+
await this.connectionManager.close();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
log.log('AMQP连接已优雅关闭');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
log.error('关闭AMQP连接时出错:', error.message);
|
|
135
|
+
}
|
|
141
136
|
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 导出兼容的API
|
|
140
|
+
module.exports = {
|
|
141
|
+
/**
|
|
142
|
+
* 初始化函数 - 保持向后兼容
|
|
143
|
+
* @param {Object} options - 配置选项
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
async init(options) {
|
|
147
|
+
const initializer = new AMQPInitializer();
|
|
148
|
+
return await initializer.init(options);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// 导出类,供需要更多控制的用户使用
|
|
152
|
+
AMQPInitializer
|
|
142
153
|
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const shelljs = require('shelljs');
|
|
2
|
+
const log = require('chalk-style');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 自动重载器模块
|
|
6
|
+
* 负责监控队列状态并在满足条件时自动重载服务
|
|
7
|
+
*/
|
|
8
|
+
class AutoReloader {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.intervalId = null;
|
|
12
|
+
this.isRunning = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 启动自动重载监控
|
|
17
|
+
* @param {Object} channel - RabbitMQ频道
|
|
18
|
+
*/
|
|
19
|
+
start(channel) {
|
|
20
|
+
const { autoReload, channelName, pmId, queryHook } = this.config;
|
|
21
|
+
|
|
22
|
+
if (autoReload <= 0) {
|
|
23
|
+
log.log('自动重载功能未启用');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (this.isRunning) {
|
|
28
|
+
log.log('自动重载监控已在运行中');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.isRunning = true;
|
|
33
|
+
log.log(`启动自动重载监控,间隔: ${autoReload}ms`);
|
|
34
|
+
|
|
35
|
+
this.intervalId = setInterval(async () => {
|
|
36
|
+
try {
|
|
37
|
+
await this._checkAndReload(channel, channelName, pmId, queryHook);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
log.error('自动重载检查时出错:', error.message);
|
|
40
|
+
}
|
|
41
|
+
}, autoReload);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 停止自动重载监控
|
|
46
|
+
*/
|
|
47
|
+
stop() {
|
|
48
|
+
if (this.intervalId) {
|
|
49
|
+
clearInterval(this.intervalId);
|
|
50
|
+
this.intervalId = null;
|
|
51
|
+
this.isRunning = false;
|
|
52
|
+
log.log('自动重载监控已停止');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 重启自动重载监控(用于重连后)
|
|
58
|
+
* @param {Object} channel - 新的RabbitMQ频道
|
|
59
|
+
*/
|
|
60
|
+
restart(channel) {
|
|
61
|
+
this.stop();
|
|
62
|
+
this.start(channel);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 获取监控状态
|
|
67
|
+
* @returns {boolean} 是否正在运行
|
|
68
|
+
*/
|
|
69
|
+
isActive() {
|
|
70
|
+
return this.isRunning;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 检查并执行重载
|
|
75
|
+
* @param {Object} channel - RabbitMQ频道
|
|
76
|
+
* @param {string} channelName - 频道名称
|
|
77
|
+
* @param {number} pmId - PM2进程ID
|
|
78
|
+
* @param {Function} queryHook - 查询钩子函数
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
async _checkAndReload(channel, channelName, pmId, queryHook) {
|
|
82
|
+
try {
|
|
83
|
+
// 检查队列中的消息数量
|
|
84
|
+
const { messageCount } = await channel.checkQueue(channelName);
|
|
85
|
+
|
|
86
|
+
// 执行查询钩子,检查是否可以重启
|
|
87
|
+
const hookResult = await queryHook();
|
|
88
|
+
|
|
89
|
+
log.log(`MQ队列中有 ${messageCount} 条消息,检测是否可以重启: ${hookResult ? 1 : 0}`);
|
|
90
|
+
|
|
91
|
+
// 只有在队列为空且钩子函数返回true时才重载
|
|
92
|
+
if (messageCount === 0 && hookResult) {
|
|
93
|
+
await this._executeReload(pmId);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
log.error('检查队列长度时出错:', error.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 执行服务重载
|
|
102
|
+
* @param {number} pmId - PM2进程ID
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
async _executeReload(pmId) {
|
|
106
|
+
try {
|
|
107
|
+
log.log('队列中没有消息,正在重载服务...');
|
|
108
|
+
|
|
109
|
+
if (shelljs && typeof shelljs.exec === 'function') {
|
|
110
|
+
const result = shelljs.exec(`pm2 reload ${pmId}`);
|
|
111
|
+
|
|
112
|
+
if (result.code === 0) {
|
|
113
|
+
log.log(`服务重载成功,PM2进程ID: ${pmId}`);
|
|
114
|
+
} else {
|
|
115
|
+
log.error(`服务重载失败,错误码: ${result.code}, 输出: ${result.stderr}`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
log.error('shelljs不可用,无法执行服务重载');
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
log.error('执行服务重载时出错:', error.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 获取当前配置信息
|
|
127
|
+
* @returns {Object} 配置信息
|
|
128
|
+
*/
|
|
129
|
+
getConfig() {
|
|
130
|
+
return {
|
|
131
|
+
autoReload: this.config.autoReload,
|
|
132
|
+
pmId: this.config.pmId,
|
|
133
|
+
isRunning: this.isRunning,
|
|
134
|
+
intervalId: this.intervalId !== null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = AutoReloader;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const log = require('chalk-style');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 配置解析器模块
|
|
6
|
+
* 负责处理配置参数和自动获取RabbitMQ连接信息
|
|
7
|
+
*/
|
|
8
|
+
class ConfigResolver {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.defaultConfig = {
|
|
11
|
+
channelName: 'node-test-channel',
|
|
12
|
+
prefetch: 1,
|
|
13
|
+
pmId: 0,
|
|
14
|
+
callback: () => {},
|
|
15
|
+
finish: () => {},
|
|
16
|
+
amqpLink: '',
|
|
17
|
+
amqpAutoLink: '',
|
|
18
|
+
heartbeat: 5,
|
|
19
|
+
timeout: 2000,
|
|
20
|
+
delay: 0,
|
|
21
|
+
autoReload: 0,
|
|
22
|
+
queryHook: () => {},
|
|
23
|
+
initHook: () => {},
|
|
24
|
+
durable: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 解析和合并配置参数
|
|
30
|
+
* @param {Object} options - 用户传入的配置选项
|
|
31
|
+
* @returns {Object} 合并后的完整配置
|
|
32
|
+
*/
|
|
33
|
+
resolveConfig(options = {}) {
|
|
34
|
+
return { ...this.defaultConfig, ...options };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 自动获取RabbitMQ连接信息
|
|
39
|
+
* @param {string} amqpAutoLink - 自动获取连接信息的HTTPS链接
|
|
40
|
+
* @param {string} fallbackAmqpLink - 备用连接地址
|
|
41
|
+
* @returns {Promise<string>} 最终使用的连接地址
|
|
42
|
+
*/
|
|
43
|
+
async resolveAmqpLink(amqpAutoLink, fallbackAmqpLink) {
|
|
44
|
+
if (!amqpAutoLink) {
|
|
45
|
+
return fallbackAmqpLink;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
log.log(`正在从 ${amqpAutoLink} 获取连接信息...`);
|
|
50
|
+
const response = await axios.post(amqpAutoLink);
|
|
51
|
+
const { info } = response.data;
|
|
52
|
+
|
|
53
|
+
if (this._validateConnectionInfo(info)) {
|
|
54
|
+
const finalAmqpLink = `amqp://${info.AMQPLIB_USER}:${info.AMQPLIB_PWD}@${info.AMQPLIB_PUB}:${info.AMQPLIB_PORT}`;
|
|
55
|
+
log.log(`✅ 自动获取连接信息成功: ${finalAmqpLink}`);
|
|
56
|
+
return finalAmqpLink;
|
|
57
|
+
} else {
|
|
58
|
+
log.error('❌ 获取的连接信息不完整,使用默认连接地址');
|
|
59
|
+
return fallbackAmqpLink;
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
log.error('❌ 获取连接信息失败:', error.message);
|
|
63
|
+
log.log('使用默认连接地址继续...');
|
|
64
|
+
return fallbackAmqpLink;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 验证连接信息的完整性
|
|
70
|
+
* @param {Object} info - 连接信息对象
|
|
71
|
+
* @returns {boolean} 是否包含所有必要字段
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
_validateConnectionInfo(info) {
|
|
75
|
+
return info &&
|
|
76
|
+
info.AMQPLIB_USER &&
|
|
77
|
+
info.AMQPLIB_PWD &&
|
|
78
|
+
info.AMQPLIB_PUB &&
|
|
79
|
+
info.AMQPLIB_PORT;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = ConfigResolver;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const amqp = require('amqplib');
|
|
2
|
+
const log = require('chalk-style');
|
|
3
|
+
const happy = require('happy-help');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 连接管理器模块
|
|
7
|
+
* 负责RabbitMQ连接管理、重连逻辑、频道管理
|
|
8
|
+
*/
|
|
9
|
+
class ConnectionManager {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.connection = null;
|
|
13
|
+
this.channel = null;
|
|
14
|
+
this.amqpLink = '';
|
|
15
|
+
this.reconnectInProgress = false; // 防止重复重连
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 设置AMQP连接地址
|
|
20
|
+
* @param {string} amqpLink - RabbitMQ连接地址
|
|
21
|
+
*/
|
|
22
|
+
setAmqpLink(amqpLink) {
|
|
23
|
+
this.amqpLink = amqpLink;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 初始化连接和频道
|
|
28
|
+
* @returns {Promise<Object>} 返回频道对象
|
|
29
|
+
*/
|
|
30
|
+
async initialize() {
|
|
31
|
+
const { channelName, durable, prefetch, heartbeat, timeout, initHook } = this.config;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// 建立连接
|
|
35
|
+
this.connection = await amqp.connect(this.amqpLink, { heartbeat, timeout });
|
|
36
|
+
|
|
37
|
+
// 创建频道
|
|
38
|
+
this.channel = await this.connection.createChannel();
|
|
39
|
+
|
|
40
|
+
// 设置队列和预取
|
|
41
|
+
await this.channel.assertQueue(channelName, { durable });
|
|
42
|
+
await this.channel.prefetch(prefetch);
|
|
43
|
+
|
|
44
|
+
// 执行初始化钩子
|
|
45
|
+
await initHook({ channel: this.channel, connection: this.connection });
|
|
46
|
+
|
|
47
|
+
log.log(`已连接到RabbitMQ,频道: ${channelName}`);
|
|
48
|
+
|
|
49
|
+
// 设置连接事件监听
|
|
50
|
+
this._setupConnectionEvents();
|
|
51
|
+
|
|
52
|
+
return this.channel;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
log.error('初始化RabbitMQ连接时出错:', error);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 重连逻辑
|
|
61
|
+
* @returns {Promise<Object>} 返回新的频道对象
|
|
62
|
+
*/
|
|
63
|
+
async reconnect() {
|
|
64
|
+
if (this.reconnectInProgress) {
|
|
65
|
+
log.log('重连已在进行中,跳过此次重连请求');
|
|
66
|
+
return this.channel;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.reconnectInProgress = true;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
log.error('连接丢失,正在尝试重连...');
|
|
73
|
+
|
|
74
|
+
// 清理旧的channel状态
|
|
75
|
+
await this._cleanupOldChannel();
|
|
76
|
+
|
|
77
|
+
// 等待重连间隔
|
|
78
|
+
await happy.sleep(2);
|
|
79
|
+
|
|
80
|
+
// 重新初始化连接
|
|
81
|
+
await this.initialize();
|
|
82
|
+
|
|
83
|
+
log.log(`已重新连接到RabbitMQ,频道: ${this.config.channelName}`);
|
|
84
|
+
|
|
85
|
+
return this.channel;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.error('重连失败:', error.message);
|
|
88
|
+
// 如果重连失败,等待一段时间后重试
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
this.reconnectInProgress = false;
|
|
91
|
+
this.reconnect();
|
|
92
|
+
}, 5000);
|
|
93
|
+
throw error;
|
|
94
|
+
} finally {
|
|
95
|
+
this.reconnectInProgress = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 获取当前频道
|
|
101
|
+
* @returns {Object|null} 当前频道对象
|
|
102
|
+
*/
|
|
103
|
+
getChannel() {
|
|
104
|
+
return this.channel;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取当前连接
|
|
109
|
+
* @returns {Object|null} 当前连接对象
|
|
110
|
+
*/
|
|
111
|
+
getConnection() {
|
|
112
|
+
return this.connection;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 检查连接是否正常
|
|
117
|
+
* @returns {boolean} 连接状态
|
|
118
|
+
*/
|
|
119
|
+
isConnected() {
|
|
120
|
+
return this.connection && this.channel && !this.connection.connection.destroyed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 关闭连接
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
async close() {
|
|
128
|
+
try {
|
|
129
|
+
if (this.channel) {
|
|
130
|
+
await this.channel.close();
|
|
131
|
+
this.channel = null;
|
|
132
|
+
}
|
|
133
|
+
if (this.connection) {
|
|
134
|
+
await this.connection.close();
|
|
135
|
+
this.connection = null;
|
|
136
|
+
}
|
|
137
|
+
log.log('RabbitMQ连接已关闭');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
log.error('关闭连接时出错:', error.message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 设置连接事件监听
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
_setupConnectionEvents() {
|
|
148
|
+
// 连接错误处理
|
|
149
|
+
this.connection.on('error', (err) => {
|
|
150
|
+
log.error('连接错误:', err.message);
|
|
151
|
+
this.reconnect().catch(error => {
|
|
152
|
+
log.error('重连失败:', error.message);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// 连接关闭处理
|
|
157
|
+
this.connection.on('close', () => {
|
|
158
|
+
log.error('与RabbitMQ的连接已关闭');
|
|
159
|
+
this.reconnect().catch(error => {
|
|
160
|
+
log.error('重连失败:', error.message);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 清理旧的频道状态
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
async _cleanupOldChannel() {
|
|
170
|
+
if (this.channel) {
|
|
171
|
+
try {
|
|
172
|
+
await this.channel.close();
|
|
173
|
+
} catch (e) {
|
|
174
|
+
log.log('关闭旧channel时出错:', e.message);
|
|
175
|
+
}
|
|
176
|
+
this.channel = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = ConnectionManager;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const log = require('chalk-style');
|
|
2
|
+
const happy = require('happy-help');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 消息处理器模块
|
|
6
|
+
* 负责消息处理、状态跟踪、ACK/REJECT逻辑
|
|
7
|
+
*/
|
|
8
|
+
class MessageProcessor {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.processingMessages = new Set(); // 跟踪正在处理的消息
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 开始消费消息
|
|
16
|
+
* @param {Object} channel - RabbitMQ频道
|
|
17
|
+
* @returns {Promise<Function>} 返回清理函数
|
|
18
|
+
*/
|
|
19
|
+
async startConsuming(channel) {
|
|
20
|
+
const { channelName, callback, delay } = this.config;
|
|
21
|
+
|
|
22
|
+
log.log(`开始消费: ${channelName}`);
|
|
23
|
+
|
|
24
|
+
await channel.consume(channelName, async (msg) => {
|
|
25
|
+
if (msg !== null) {
|
|
26
|
+
await this._handleMessage(msg, channel, callback, delay);
|
|
27
|
+
} else {
|
|
28
|
+
// ✅ 修复关键bug:null消息不需要ACK操作
|
|
29
|
+
log.log('收到null消息,队列可能为空或连接中断');
|
|
30
|
+
// 移除错误的 channel.ack(msg) 调用
|
|
31
|
+
}
|
|
32
|
+
}, { noAck: false });
|
|
33
|
+
|
|
34
|
+
// 返回清理函数,用于重连时清理状态
|
|
35
|
+
return () => {
|
|
36
|
+
this.processingMessages.clear();
|
|
37
|
+
log.log('已清理消息处理状态');
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 处理单条消息
|
|
43
|
+
* @param {Object} msg - RabbitMQ消息对象
|
|
44
|
+
* @param {Object} channel - RabbitMQ频道
|
|
45
|
+
* @param {Function} callback - 消息处理回调函数
|
|
46
|
+
* @param {number} delay - 延迟ACK时间
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
async _handleMessage(msg, channel, callback, delay) {
|
|
50
|
+
const deliveryTag = msg.fields.deliveryTag;
|
|
51
|
+
|
|
52
|
+
// 检查消息是否已在处理中
|
|
53
|
+
if (this.processingMessages.has(deliveryTag)) {
|
|
54
|
+
log.log(`消息 ${deliveryTag} 已在处理中,跳过`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.processingMessages.add(deliveryTag);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = JSON.parse(msg.content.toString());
|
|
62
|
+
log.log(`🪴 队列收到消息: ${JSON.stringify(content)}`);
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
|
|
65
|
+
// 执行消息处理回调
|
|
66
|
+
callback(content)
|
|
67
|
+
.then(() => {
|
|
68
|
+
const endTime = Date.now() - startTime;
|
|
69
|
+
log.log(`☘️ 消息处理完成,延迟: ${delay}ms,总时间: ${endTime}ms`);
|
|
70
|
+
this._acknowledgeMessage(msg, channel, deliveryTag, delay);
|
|
71
|
+
})
|
|
72
|
+
.catch(async (e) => {
|
|
73
|
+
log.log('‼️ 处理消息返回错误:', e);
|
|
74
|
+
await this._rejectMessage(msg, channel, deliveryTag);
|
|
75
|
+
});
|
|
76
|
+
} catch (e) {
|
|
77
|
+
log.log('‼️ 处理消息时出错:', e.message);
|
|
78
|
+
await this._rejectMessage(msg, channel, deliveryTag);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 确认消息
|
|
84
|
+
* @param {Object} msg - RabbitMQ消息对象
|
|
85
|
+
* @param {Object} channel - RabbitMQ频道
|
|
86
|
+
* @param {string} deliveryTag - 消息标签
|
|
87
|
+
* @param {number} delay - 延迟时间
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
_acknowledgeMessage(msg, channel, deliveryTag, delay) {
|
|
91
|
+
// 确保消息还在处理集合中再进行ACK
|
|
92
|
+
if (this.processingMessages.has(deliveryTag)) {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
try {
|
|
95
|
+
channel.ack(msg);
|
|
96
|
+
this.processingMessages.delete(deliveryTag);
|
|
97
|
+
log.log(`✅ 消息 ${deliveryTag} 已确认`);
|
|
98
|
+
} catch (ackError) {
|
|
99
|
+
log.error(`ACK消息 ${deliveryTag} 时出错:`, ackError.message);
|
|
100
|
+
this.processingMessages.delete(deliveryTag);
|
|
101
|
+
}
|
|
102
|
+
}, delay);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 拒绝消息并重新排队
|
|
108
|
+
* @param {Object} msg - RabbitMQ消息对象
|
|
109
|
+
* @param {Object} channel - RabbitMQ频道
|
|
110
|
+
* @param {string} deliveryTag - 消息标签
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
async _rejectMessage(msg, channel, deliveryTag) {
|
|
114
|
+
await happy.sleep(5);
|
|
115
|
+
|
|
116
|
+
if (this.processingMessages.has(deliveryTag)) {
|
|
117
|
+
try {
|
|
118
|
+
channel.reject(msg, true);
|
|
119
|
+
this.processingMessages.delete(deliveryTag);
|
|
120
|
+
log.log(`❌ 消息 ${deliveryTag} 已拒绝并重新排队`);
|
|
121
|
+
} catch (rejectError) {
|
|
122
|
+
log.error(`拒绝消息 ${deliveryTag} 时出错:`, rejectError.message);
|
|
123
|
+
this.processingMessages.delete(deliveryTag);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 清理所有处理中的消息状态
|
|
130
|
+
*/
|
|
131
|
+
clearProcessingMessages() {
|
|
132
|
+
this.processingMessages.clear();
|
|
133
|
+
log.log('已清理所有消息处理状态');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 获取当前处理中的消息数量
|
|
138
|
+
* @returns {number} 处理中的消息数量
|
|
139
|
+
*/
|
|
140
|
+
getProcessingCount() {
|
|
141
|
+
return this.processingMessages.size;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = MessageProcessor;
|