sycommon-python-lib 0.1.9__py3-none-any.whl → 0.1.11__py3-none-any.whl
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.
Potentially problematic release.
This version of sycommon-python-lib might be problematic. Click here for more details.
- command/cli.py +167 -0
- sycommon/middleware/docs.py +30 -0
- sycommon/middleware/middleware.py +4 -0
- sycommon/middleware/traceid.py +32 -4
- sycommon/models/base_http.py +27 -25
- sycommon/rabbitmq/rabbitmq_client.py +557 -401
- sycommon/rabbitmq/rabbitmq_service.py +443 -280
- sycommon/services.py +16 -9
- sycommon/synacos/feign.py +338 -126
- sycommon/tools/docs.py +42 -0
- {sycommon_python_lib-0.1.9.dist-info → sycommon_python_lib-0.1.11.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.1.9.dist-info → sycommon_python_lib-0.1.11.dist-info}/RECORD +15 -11
- sycommon_python_lib-0.1.11.dist-info/entry_points.txt +2 -0
- {sycommon_python_lib-0.1.9.dist-info → sycommon_python_lib-0.1.11.dist-info}/top_level.txt +1 -0
- {sycommon_python_lib-0.1.9.dist-info → sycommon_python_lib-0.1.11.dist-info}/WHEEL +0 -0
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
-
import aio_pika
|
|
4
3
|
import json
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from typing import Callable, Coroutine, Optional, Dict, Any, Union, Set, List
|
|
5
|
+
from aio_pika import connect_robust, Message, DeliveryMode, ExchangeType
|
|
6
|
+
from aio_pika.abc import (
|
|
7
|
+
AbstractConnection,
|
|
8
|
+
AbstractChannel,
|
|
9
|
+
AbstractExchange,
|
|
10
|
+
AbstractQueue,
|
|
11
|
+
AbstractIncomingMessage,
|
|
12
|
+
ConsumerTag
|
|
13
|
+
)
|
|
14
|
+
from aiormq.exceptions import ChannelInvalidStateError, ConnectionClosed
|
|
7
15
|
|
|
8
16
|
from sycommon.models.mqmsg_model import MQMsgModel
|
|
9
|
-
from aiormq.exceptions import ChannelInvalidStateError, ConnectionClosed
|
|
10
17
|
|
|
11
18
|
# 最大重试次数限制
|
|
12
19
|
MAX_RETRY_COUNT = 3
|
|
13
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
14
23
|
|
|
15
24
|
class RabbitMQClient:
|
|
25
|
+
"""
|
|
26
|
+
RabbitMQ客户端,支持集群多节点配置,基于aio-pika实现
|
|
27
|
+
提供自动故障转移、连接恢复和消息可靠性保障
|
|
28
|
+
"""
|
|
29
|
+
|
|
16
30
|
def __init__(
|
|
17
31
|
self,
|
|
18
|
-
|
|
32
|
+
hosts: List[str],
|
|
19
33
|
port: int,
|
|
20
34
|
username: str,
|
|
21
35
|
password: str,
|
|
@@ -31,61 +45,105 @@ class RabbitMQClient:
|
|
|
31
45
|
connection_timeout: int = 10,
|
|
32
46
|
rpc_timeout: int = 10,
|
|
33
47
|
app_name: str = "",
|
|
34
|
-
reconnection_delay: int =
|
|
48
|
+
reconnection_delay: int = 1,
|
|
35
49
|
max_reconnection_attempts: int = 5,
|
|
36
|
-
heartbeat: int =
|
|
37
|
-
|
|
50
|
+
heartbeat: int = 60,
|
|
51
|
+
prefetch_count: int = 2,
|
|
52
|
+
message_process_timeout: int = 30,
|
|
53
|
+
consumption_stall_threshold: int = 120
|
|
38
54
|
):
|
|
39
|
-
"""
|
|
40
|
-
|
|
55
|
+
"""
|
|
56
|
+
初始化RabbitMQ客户端,支持集群多节点配置
|
|
57
|
+
|
|
58
|
+
:param hosts: RabbitMQ主机地址列表(集群节点)
|
|
59
|
+
:param port: RabbitMQ端口
|
|
60
|
+
:param username: 用户名
|
|
61
|
+
:param password: 密码
|
|
62
|
+
:param virtualhost: 虚拟主机
|
|
63
|
+
:param exchange_name: 交换机名称
|
|
64
|
+
:param exchange_type: 交换机类型
|
|
65
|
+
:param queue_name: 队列名称
|
|
66
|
+
:param routing_key: 路由键
|
|
67
|
+
:param durable: 是否持久化
|
|
68
|
+
:param auto_delete: 是否自动删除
|
|
69
|
+
:param auto_parse_json: 是否自动解析JSON消息
|
|
70
|
+
:param create_if_not_exists: 如果资源不存在是否创建
|
|
71
|
+
:param connection_timeout: 连接超时时间(秒)
|
|
72
|
+
:param rpc_timeout: RPC操作超时时间(秒)
|
|
73
|
+
:param app_name: 应用名称,用于标识连接
|
|
74
|
+
:param reconnection_delay: 重连延迟(秒)
|
|
75
|
+
:param max_reconnection_attempts: 最大重连尝试次数
|
|
76
|
+
:param heartbeat: 心跳间隔(秒)
|
|
77
|
+
:param prefetch_count: 预取消息数量
|
|
78
|
+
:param message_process_timeout: 消息处理超时时间(秒)
|
|
79
|
+
:param consumption_stall_threshold: 消费停滞检测阈值(秒)
|
|
80
|
+
"""
|
|
81
|
+
# 连接参数 - 支持多主机
|
|
82
|
+
self.hosts = [host.strip() for host in hosts if host.strip()]
|
|
83
|
+
if not self.hosts:
|
|
84
|
+
raise ValueError("至少需要提供一个RabbitMQ主机地址")
|
|
41
85
|
self.port = port
|
|
42
86
|
self.username = username
|
|
43
87
|
self.password = password
|
|
44
88
|
self.virtualhost = virtualhost
|
|
89
|
+
self.app_name = app_name or "rabbitmq-client"
|
|
90
|
+
|
|
91
|
+
# 交换器和队列参数
|
|
45
92
|
self.exchange_name = exchange_name
|
|
46
93
|
self.exchange_type = ExchangeType(exchange_type)
|
|
47
94
|
self.queue_name = queue_name
|
|
48
95
|
self.routing_key = routing_key
|
|
49
96
|
self.durable = durable
|
|
50
97
|
self.auto_delete = auto_delete
|
|
98
|
+
|
|
99
|
+
# 行为控制参数
|
|
51
100
|
self.auto_parse_json = auto_parse_json
|
|
52
101
|
self.create_if_not_exists = create_if_not_exists
|
|
53
102
|
self.connection_timeout = connection_timeout
|
|
54
103
|
self.rpc_timeout = rpc_timeout
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
# 连接保活相关配置
|
|
58
|
-
self.heartbeat = heartbeat
|
|
59
|
-
self.keepalive_interval = keepalive_interval
|
|
60
|
-
self.last_activity_timestamp = asyncio.get_event_loop().time()
|
|
104
|
+
self.prefetch_count = prefetch_count
|
|
61
105
|
|
|
62
|
-
#
|
|
106
|
+
# 重连和保活参数
|
|
63
107
|
self.reconnection_delay = reconnection_delay
|
|
64
108
|
self.max_reconnection_attempts = max_reconnection_attempts
|
|
109
|
+
self.heartbeat = heartbeat
|
|
65
110
|
|
|
66
|
-
#
|
|
67
|
-
self.
|
|
68
|
-
self.
|
|
69
|
-
self.exchange: Optional[aio_pika.Exchange] = None
|
|
70
|
-
self.queue: Optional[aio_pika.Queue] = None
|
|
111
|
+
# 消息处理参数
|
|
112
|
+
self.message_process_timeout = message_process_timeout
|
|
113
|
+
self.consumption_stall_threshold = consumption_stall_threshold
|
|
71
114
|
|
|
115
|
+
# 连接和通道对象
|
|
116
|
+
self.connection: Optional[AbstractConnection] = None
|
|
117
|
+
self.channel: Optional[AbstractChannel] = None
|
|
118
|
+
self.exchange: Optional[AbstractExchange] = None
|
|
119
|
+
self.queue: Optional[AbstractQueue] = None
|
|
120
|
+
|
|
121
|
+
# 当前活跃连接的主机
|
|
122
|
+
self._active_host: Optional[str] = None
|
|
123
|
+
|
|
124
|
+
# 状态跟踪
|
|
125
|
+
self.actual_queue_name: Optional[str] = None
|
|
72
126
|
self._exchange_exists = False
|
|
73
127
|
self._queue_exists = False
|
|
74
128
|
self._queue_bound = False
|
|
129
|
+
self._is_consuming = False
|
|
130
|
+
self._closed = False
|
|
131
|
+
self._consumer_tag: Optional[ConsumerTag] = None
|
|
132
|
+
self._last_activity_timestamp = asyncio.get_event_loop().time()
|
|
133
|
+
self._last_message_processed = asyncio.get_event_loop().time()
|
|
75
134
|
|
|
76
|
-
#
|
|
135
|
+
# 任务和处理器
|
|
77
136
|
self.message_handler: Optional[Callable[
|
|
78
|
-
[Union[
|
|
79
|
-
Coroutine
|
|
137
|
+
[Union[Dict[str, Any], str], AbstractIncomingMessage],
|
|
138
|
+
Coroutine[Any, Any, None]
|
|
80
139
|
]] = None
|
|
81
|
-
|
|
82
|
-
# 消费相关
|
|
83
|
-
self._consumer_tag: Optional[str] = None
|
|
84
140
|
self._consuming_task: Optional[asyncio.Task] = None
|
|
85
|
-
self._is_consuming: bool = False
|
|
86
141
|
self._reconnect_task: Optional[asyncio.Task] = None
|
|
87
|
-
self.
|
|
88
|
-
self.
|
|
142
|
+
self._keepalive_task: Optional[asyncio.Task] = None
|
|
143
|
+
self._monitor_task: Optional[asyncio.Task] = None
|
|
144
|
+
|
|
145
|
+
# 消息处理跟踪
|
|
146
|
+
self._processing_message_ids: Set[str] = set()
|
|
89
147
|
|
|
90
148
|
@property
|
|
91
149
|
def is_connected(self) -> bool:
|
|
@@ -94,18 +152,24 @@ class RabbitMQClient:
|
|
|
94
152
|
self.connection is not None and
|
|
95
153
|
not self.connection.is_closed and
|
|
96
154
|
self.channel is not None and
|
|
97
|
-
not self.channel.is_closed
|
|
155
|
+
not self.channel.is_closed and
|
|
156
|
+
self.exchange is not None)
|
|
98
157
|
|
|
99
|
-
def _update_activity_timestamp(self):
|
|
158
|
+
def _update_activity_timestamp(self) -> None:
|
|
100
159
|
"""更新最后活动时间戳"""
|
|
101
|
-
self.
|
|
160
|
+
self._last_activity_timestamp = asyncio.get_event_loop().time()
|
|
161
|
+
|
|
162
|
+
def _update_message_processed_timestamp(self) -> None:
|
|
163
|
+
"""更新最后消息处理时间戳"""
|
|
164
|
+
self._last_message_processed = asyncio.get_event_loop().time()
|
|
102
165
|
|
|
103
166
|
async def _check_exchange_exists(self) -> bool:
|
|
104
|
-
"""
|
|
167
|
+
"""检查交换机是否存在"""
|
|
105
168
|
if not self.channel:
|
|
106
169
|
return False
|
|
107
170
|
|
|
108
171
|
try:
|
|
172
|
+
# 使用被动模式检查交换机是否存在
|
|
109
173
|
await asyncio.wait_for(
|
|
110
174
|
self.channel.declare_exchange(
|
|
111
175
|
name=self.exchange_name,
|
|
@@ -118,18 +182,21 @@ class RabbitMQClient:
|
|
|
118
182
|
self._update_activity_timestamp()
|
|
119
183
|
return True
|
|
120
184
|
except asyncio.TimeoutError:
|
|
121
|
-
|
|
185
|
+
logger.error(
|
|
186
|
+
f"检查交换机 '{self.exchange_name}' 超时 (主机: {self._active_host})")
|
|
122
187
|
return False
|
|
123
188
|
except Exception as e:
|
|
124
|
-
|
|
189
|
+
logger.debug(
|
|
190
|
+
f"交换机 '{self.exchange_name}' 不存在: {str(e)} (主机: {self._active_host})")
|
|
125
191
|
return False
|
|
126
192
|
|
|
127
193
|
async def _check_queue_exists(self) -> bool:
|
|
128
|
-
"""
|
|
194
|
+
"""检查队列是否存在"""
|
|
129
195
|
if not self.channel or not self.queue_name:
|
|
130
196
|
return False
|
|
131
197
|
|
|
132
198
|
try:
|
|
199
|
+
# 使用被动模式检查队列是否存在
|
|
133
200
|
await asyncio.wait_for(
|
|
134
201
|
self.channel.declare_queue(
|
|
135
202
|
name=self.queue_name,
|
|
@@ -141,21 +208,24 @@ class RabbitMQClient:
|
|
|
141
208
|
self._update_activity_timestamp()
|
|
142
209
|
return True
|
|
143
210
|
except asyncio.TimeoutError:
|
|
144
|
-
|
|
211
|
+
logger.error(
|
|
212
|
+
f"检查队列 '{self.queue_name}' 超时 (主机: {self._active_host})")
|
|
145
213
|
return False
|
|
146
214
|
except Exception as e:
|
|
147
|
-
|
|
215
|
+
logger.debug(
|
|
216
|
+
f"队列 '{self.queue_name}' 不存在: {str(e)} (主机: {self._active_host})")
|
|
148
217
|
return False
|
|
149
218
|
|
|
150
219
|
async def _bind_queue(self) -> bool:
|
|
151
|
-
"""
|
|
220
|
+
"""将队列绑定到交换机"""
|
|
152
221
|
if not self.channel or not self.queue or not self.exchange:
|
|
153
222
|
return False
|
|
154
223
|
|
|
155
|
-
retries = 2
|
|
224
|
+
retries = 2
|
|
225
|
+
bind_routing_key = self.routing_key if self.routing_key else '#'
|
|
226
|
+
|
|
156
227
|
for attempt in range(retries + 1):
|
|
157
228
|
try:
|
|
158
|
-
bind_routing_key = self.routing_key if self.routing_key else '#'
|
|
159
229
|
await asyncio.wait_for(
|
|
160
230
|
self.queue.bind(
|
|
161
231
|
self.exchange,
|
|
@@ -165,319 +235,378 @@ class RabbitMQClient:
|
|
|
165
235
|
)
|
|
166
236
|
self._queue_bound = True
|
|
167
237
|
self._update_activity_timestamp()
|
|
168
|
-
|
|
169
|
-
f"队列 '{self.queue_name}' 已绑定到交换机 '{self.exchange_name}',路由键: {bind_routing_key}")
|
|
238
|
+
logger.info(
|
|
239
|
+
f"队列 '{self.queue_name}' 已绑定到交换机 '{self.exchange_name}',路由键: {bind_routing_key} (主机: {self._active_host})")
|
|
170
240
|
return True
|
|
171
241
|
except asyncio.TimeoutError:
|
|
172
|
-
|
|
173
|
-
f"队列 '{self.queue_name}' 绑定超时(第{attempt+1}次尝试)")
|
|
174
|
-
if attempt >= retries:
|
|
175
|
-
self._queue_bound = False
|
|
176
|
-
return False
|
|
177
|
-
await asyncio.sleep(1)
|
|
242
|
+
logger.warning(
|
|
243
|
+
f"队列 '{self.queue_name}' 绑定超时(第{attempt+1}次尝试)(主机: {self._active_host})")
|
|
178
244
|
except Exception as e:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
245
|
+
logger.error(
|
|
246
|
+
f"队列绑定失败(第{attempt+1}次尝试): {str(e)} (主机: {self._active_host})")
|
|
247
|
+
|
|
248
|
+
if attempt < retries:
|
|
183
249
|
await asyncio.sleep(1)
|
|
250
|
+
|
|
251
|
+
self._queue_bound = False
|
|
184
252
|
return False
|
|
185
253
|
|
|
254
|
+
async def _try_connect_host(self, host: str) -> AbstractConnection:
|
|
255
|
+
"""尝试连接单个主机"""
|
|
256
|
+
try:
|
|
257
|
+
logger.debug(f"尝试连接主机: {host}:{self.port}")
|
|
258
|
+
return await asyncio.wait_for(
|
|
259
|
+
connect_robust(
|
|
260
|
+
host=host,
|
|
261
|
+
port=self.port,
|
|
262
|
+
login=self.username,
|
|
263
|
+
password=self.password,
|
|
264
|
+
virtualhost=self.virtualhost,
|
|
265
|
+
heartbeat=self.heartbeat,
|
|
266
|
+
client_properties={
|
|
267
|
+
"connection_name": f"{self.app_name}@{host}"
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
timeout=self.connection_timeout
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.warning(f"连接主机 {host}:{self.port} 失败: {str(e)}")
|
|
274
|
+
raise
|
|
275
|
+
|
|
186
276
|
async def connect(self, force_reconnect: bool = False, declare_queue: bool = True) -> None:
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
277
|
+
"""
|
|
278
|
+
建立与RabbitMQ集群的连接(支持多节点故障转移)并初始化所需资源
|
|
279
|
+
|
|
280
|
+
:param force_reconnect: 是否强制重新连接
|
|
281
|
+
:param declare_queue: 是否声明队列
|
|
282
|
+
"""
|
|
283
|
+
logger.debug(
|
|
284
|
+
f"连接参数 - force_reconnect={force_reconnect}, "
|
|
285
|
+
f"declare_queue={declare_queue}, create_if_not_exists={self.create_if_not_exists}, "
|
|
286
|
+
f"主机列表: {self.hosts}"
|
|
192
287
|
)
|
|
193
288
|
|
|
289
|
+
# 如果已连接且不强制重连,则直接返回
|
|
194
290
|
if self.is_connected and not force_reconnect:
|
|
195
291
|
return
|
|
196
292
|
|
|
197
|
-
#
|
|
293
|
+
# 取消正在进行的重连任务
|
|
198
294
|
if self._reconnect_task and not self._reconnect_task.done():
|
|
199
295
|
self._reconnect_task.cancel()
|
|
200
296
|
|
|
201
|
-
|
|
202
|
-
f"尝试连接RabbitMQ -
|
|
203
|
-
f"虚拟主机: {self.virtualhost}, "
|
|
204
|
-
f"队列: {self.queue_name}, "
|
|
205
|
-
f"声明队列: {declare_queue}, "
|
|
206
|
-
f"允许创建: {self.create_if_not_exists}"
|
|
297
|
+
logger.debug(
|
|
298
|
+
f"尝试连接RabbitMQ集群 - 主机数量: {len(self.hosts)}, "
|
|
299
|
+
f"虚拟主机: {self.virtualhost}, 队列: {self.queue_name}"
|
|
207
300
|
)
|
|
208
301
|
|
|
209
302
|
# 重置状态
|
|
210
303
|
self._exchange_exists = False
|
|
211
304
|
self._queue_exists = False
|
|
212
305
|
self._queue_bound = False
|
|
306
|
+
self._active_host = None
|
|
213
307
|
|
|
214
308
|
retries = 0
|
|
215
309
|
last_exception = None
|
|
216
310
|
|
|
217
|
-
while retries <
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
self.connection = await asyncio.wait_for(
|
|
225
|
-
aio_pika.connect_robust(
|
|
226
|
-
host=self.host,
|
|
227
|
-
port=self.port,
|
|
228
|
-
login=self.username,
|
|
229
|
-
password=self.password,
|
|
230
|
-
virtualhost=self.virtualhost,
|
|
231
|
-
heartbeat=self.heartbeat,
|
|
232
|
-
client_properties={
|
|
233
|
-
"connection_name": self.app_name or "rabbitmq-client"}
|
|
234
|
-
),
|
|
235
|
-
timeout=self.connection_timeout
|
|
236
|
-
)
|
|
311
|
+
while retries < self.max_reconnection_attempts:
|
|
312
|
+
# 遍历所有主机尝试连接(故障转移)
|
|
313
|
+
for host in self.hosts:
|
|
314
|
+
try:
|
|
315
|
+
# 关闭现有连接
|
|
316
|
+
if self.connection and not self.connection.is_closed:
|
|
317
|
+
await self.connection.close()
|
|
237
318
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
self.
|
|
241
|
-
timeout=self.rpc_timeout
|
|
242
|
-
)
|
|
243
|
-
await self.channel.set_qos(prefetch_count=2)
|
|
319
|
+
# 尝试连接当前主机
|
|
320
|
+
self.connection = await self._try_connect_host(host)
|
|
321
|
+
self._active_host = host
|
|
244
322
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if self.create_if_not_exists:
|
|
249
|
-
# 创建交换机
|
|
250
|
-
self.exchange = await asyncio.wait_for(
|
|
251
|
-
self.channel.declare_exchange(
|
|
252
|
-
name=self.exchange_name,
|
|
253
|
-
type=self.exchange_type,
|
|
254
|
-
durable=self.durable,
|
|
255
|
-
auto_delete=self.auto_delete
|
|
256
|
-
),
|
|
257
|
-
timeout=self.rpc_timeout
|
|
258
|
-
)
|
|
259
|
-
self._exchange_exists = True
|
|
260
|
-
logging.info(f"已创建交换机 '{self.exchange_name}'")
|
|
261
|
-
else:
|
|
262
|
-
raise Exception(
|
|
263
|
-
f"交换机 '{self.exchange_name}' 不存在且不允许自动创建")
|
|
264
|
-
else:
|
|
265
|
-
# 获取已有交换机
|
|
266
|
-
self.exchange = await asyncio.wait_for(
|
|
267
|
-
self.channel.get_exchange(self.exchange_name),
|
|
323
|
+
# 创建通道
|
|
324
|
+
self.channel = await asyncio.wait_for(
|
|
325
|
+
self.connection.channel(),
|
|
268
326
|
timeout=self.rpc_timeout
|
|
269
327
|
)
|
|
270
|
-
logging.info(f"使用已存在的交换机 '{self.exchange_name}'")
|
|
271
|
-
|
|
272
|
-
# 2. 处理队列 - 只有declare_queue为True时才处理
|
|
273
|
-
if declare_queue and self.queue_name:
|
|
274
|
-
queue_exists = await self._check_queue_exists()
|
|
275
328
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
329
|
+
# 设置预取计数,控制消息公平分发
|
|
330
|
+
await self.channel.set_qos(prefetch_count=self.prefetch_count)
|
|
331
|
+
|
|
332
|
+
# 处理交换机
|
|
333
|
+
exchange_exists = await self._check_exchange_exists()
|
|
334
|
+
if not exchange_exists:
|
|
335
|
+
if self.create_if_not_exists:
|
|
336
|
+
# 创建交换机
|
|
337
|
+
self.exchange = await asyncio.wait_for(
|
|
338
|
+
self.channel.declare_exchange(
|
|
339
|
+
name=self.exchange_name,
|
|
340
|
+
type=self.exchange_type,
|
|
341
|
+
durable=self.durable,
|
|
342
|
+
auto_delete=self.auto_delete
|
|
343
|
+
),
|
|
344
|
+
timeout=self.rpc_timeout
|
|
345
|
+
)
|
|
346
|
+
self._exchange_exists = True
|
|
347
|
+
logger.info(
|
|
348
|
+
f"已创建交换机 '{self.exchange_name}' (主机: {self._active_host})")
|
|
349
|
+
else:
|
|
279
350
|
raise Exception(
|
|
280
|
-
f"
|
|
281
|
-
|
|
282
|
-
# 创建队列
|
|
283
|
-
self.queue = await asyncio.wait_for(
|
|
284
|
-
self.channel.declare_queue(
|
|
285
|
-
name=self.queue_name,
|
|
286
|
-
durable=self.durable,
|
|
287
|
-
auto_delete=self.auto_delete,
|
|
288
|
-
exclusive=False,
|
|
289
|
-
passive=False
|
|
290
|
-
),
|
|
291
|
-
timeout=self.rpc_timeout
|
|
292
|
-
)
|
|
293
|
-
self._queue_exists = True
|
|
294
|
-
logging.info(f"已创建队列 '{self.queue_name}'")
|
|
351
|
+
f"交换机 '{self.exchange_name}' 不存在且不允许自动创建 (主机: {self._active_host})")
|
|
295
352
|
else:
|
|
296
|
-
#
|
|
297
|
-
self.
|
|
298
|
-
self.channel.
|
|
353
|
+
# 获取已有交换机
|
|
354
|
+
self.exchange = await asyncio.wait_for(
|
|
355
|
+
self.channel.get_exchange(self.exchange_name),
|
|
299
356
|
timeout=self.rpc_timeout
|
|
300
357
|
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
358
|
+
logger.info(
|
|
359
|
+
f"使用已存在的交换机 '{self.exchange_name}' (主机: {self._active_host})")
|
|
360
|
+
|
|
361
|
+
# 处理队列
|
|
362
|
+
if declare_queue and self.queue_name:
|
|
363
|
+
queue_exists = await self._check_queue_exists()
|
|
364
|
+
|
|
365
|
+
if not queue_exists:
|
|
366
|
+
if not self.create_if_not_exists:
|
|
367
|
+
raise Exception(
|
|
368
|
+
f"队列 '{self.queue_name}' 不存在且不允许自动创建 (主机: {self._active_host})")
|
|
369
|
+
|
|
370
|
+
# 创建队列
|
|
371
|
+
self.queue = await asyncio.wait_for(
|
|
372
|
+
self.channel.declare_queue(
|
|
373
|
+
name=self.queue_name,
|
|
374
|
+
durable=self.durable,
|
|
375
|
+
auto_delete=self.auto_delete,
|
|
376
|
+
exclusive=False
|
|
377
|
+
),
|
|
378
|
+
timeout=self.rpc_timeout
|
|
379
|
+
)
|
|
380
|
+
self._queue_exists = True
|
|
381
|
+
self.actual_queue_name = self.queue_name
|
|
382
|
+
logger.info(
|
|
383
|
+
f"已创建队列 '{self.queue_name}' (主机: {self._active_host})")
|
|
384
|
+
else:
|
|
385
|
+
# 获取已有队列
|
|
386
|
+
self.queue = await asyncio.wait_for(
|
|
387
|
+
self.channel.get_queue(self.queue_name),
|
|
388
|
+
timeout=self.rpc_timeout
|
|
389
|
+
)
|
|
390
|
+
self.actual_queue_name = self.queue_name
|
|
391
|
+
logger.info(
|
|
392
|
+
f"使用已存在的队列 '{self.queue_name}' (主机: {self._active_host})")
|
|
393
|
+
|
|
394
|
+
# 绑定队列到交换机
|
|
395
|
+
if self.queue and self.exchange:
|
|
396
|
+
bound = await self._bind_queue()
|
|
397
|
+
if not bound:
|
|
398
|
+
raise Exception(
|
|
399
|
+
f"队列 '{self.queue_name}' 绑定到交换机 '{self.exchange_name}' 失败 (主机: {self._active_host})")
|
|
400
|
+
else:
|
|
307
401
|
raise Exception(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
402
|
+
"队列或交换机未正确初始化 (主机: {self._active_host})")
|
|
403
|
+
else:
|
|
404
|
+
# 不声明队列时的状态处理
|
|
405
|
+
self.queue = None
|
|
406
|
+
self.actual_queue_name = None
|
|
407
|
+
self._queue_exists = False
|
|
408
|
+
self._queue_bound = False
|
|
409
|
+
logger.debug(
|
|
410
|
+
f"跳过队列 '{self.queue_name}' 的声明和绑定 (主机: {self._active_host})")
|
|
411
|
+
|
|
412
|
+
# 验证连接状态
|
|
413
|
+
if not self.is_connected:
|
|
414
|
+
raise Exception(
|
|
415
|
+
f"连接验证失败,状态异常 (主机: {self._active_host})")
|
|
315
416
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
417
|
+
# 如果之前在消费,重新开始消费
|
|
418
|
+
if self._is_consuming and self.message_handler:
|
|
419
|
+
await self.start_consuming()
|
|
319
420
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
421
|
+
# 启动连接监控和保活任务
|
|
422
|
+
self._start_monitoring()
|
|
423
|
+
self._start_keepalive()
|
|
323
424
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
425
|
+
self._update_activity_timestamp()
|
|
426
|
+
logger.info(
|
|
427
|
+
f"RabbitMQ客户端连接成功 (主机: {self._active_host}, 队列: {self.actual_queue_name})")
|
|
428
|
+
return
|
|
328
429
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
430
|
+
except Exception as e:
|
|
431
|
+
last_exception = e
|
|
432
|
+
logger.warning(
|
|
433
|
+
f"主机 {host} 连接处理失败: {str(e)},尝试下一个主机...")
|
|
434
|
+
# 清理当前失败的连接资源
|
|
435
|
+
if self.connection and not self.connection.is_closed:
|
|
436
|
+
await self.connection.close()
|
|
437
|
+
self.connection = None
|
|
438
|
+
self.channel = None
|
|
439
|
+
self.exchange = None
|
|
440
|
+
self.queue = None
|
|
441
|
+
|
|
442
|
+
# 所有主机都尝试失败,进行重试
|
|
443
|
+
retries += 1
|
|
444
|
+
logger.warning(
|
|
445
|
+
f"集群连接失败({retries}/{self.max_reconnection_attempts}),所有主机均无法连接,重试中...")
|
|
334
446
|
|
|
335
|
-
if retries <
|
|
447
|
+
if retries < self.max_reconnection_attempts:
|
|
336
448
|
await asyncio.sleep(self.reconnection_delay)
|
|
337
449
|
|
|
338
|
-
|
|
450
|
+
logger.error(f"最终连接失败: {str(last_exception)}")
|
|
339
451
|
raise Exception(
|
|
340
|
-
f"经过
|
|
452
|
+
f"经过{self.max_reconnection_attempts}次重试后仍无法连接到RabbitMQ集群。最后错误: {str(last_exception)}")
|
|
341
453
|
|
|
342
|
-
def
|
|
343
|
-
"""
|
|
344
|
-
if self._closed:
|
|
454
|
+
def _start_monitoring(self) -> None:
|
|
455
|
+
"""启动连接和消费监控任务,支持集群节点故障检测"""
|
|
456
|
+
if self._closed or (self._monitor_task and not self._monitor_task.done()):
|
|
345
457
|
return
|
|
346
458
|
|
|
347
|
-
async def
|
|
459
|
+
async def monitor():
|
|
348
460
|
while not self._closed and self.connection:
|
|
349
461
|
try:
|
|
350
462
|
# 检查连接状态
|
|
351
463
|
if self.connection.is_closed:
|
|
352
|
-
|
|
464
|
+
logger.warning(
|
|
465
|
+
f"检测到RabbitMQ连接已关闭 (主机: {self._active_host}),将尝试重连到集群其他节点")
|
|
353
466
|
await self._schedule_reconnect()
|
|
354
467
|
return
|
|
355
468
|
|
|
356
469
|
# 检查通道状态
|
|
357
470
|
if self.channel and self.channel.is_closed:
|
|
358
|
-
|
|
471
|
+
logger.warning(
|
|
472
|
+
f"检测到RabbitMQ通道已关闭 (主机: {self._active_host}),将尝试重建")
|
|
359
473
|
await self._recreate_channel()
|
|
360
474
|
continue
|
|
475
|
+
|
|
476
|
+
# 检查消费停滞
|
|
477
|
+
if self._is_consuming:
|
|
478
|
+
current_time = asyncio.get_event_loop().time()
|
|
479
|
+
if current_time - self._last_message_processed > self.consumption_stall_threshold:
|
|
480
|
+
logger.warning(
|
|
481
|
+
f"检测到消费停滞超过 {self.consumption_stall_threshold} 秒 (主机: {self._active_host}),将重启消费者")
|
|
482
|
+
if self._is_consuming and self.message_handler:
|
|
483
|
+
await self.stop_consuming()
|
|
484
|
+
await self.start_consuming()
|
|
485
|
+
logger.info("消费者已重启以恢复消费")
|
|
361
486
|
except Exception as e:
|
|
362
|
-
|
|
487
|
+
logger.error(f"监控任务出错: {str(e)}")
|
|
363
488
|
await asyncio.sleep(1)
|
|
364
489
|
|
|
365
|
-
await asyncio.sleep(5)
|
|
490
|
+
await asyncio.sleep(5) # 每5秒检查一次
|
|
366
491
|
|
|
367
|
-
|
|
368
|
-
asyncio.create_task(monitor_task())
|
|
492
|
+
self._monitor_task = asyncio.create_task(monitor())
|
|
369
493
|
|
|
370
|
-
async def _recreate_channel(self):
|
|
371
|
-
"""
|
|
494
|
+
async def _recreate_channel(self) -> None:
|
|
495
|
+
"""重建通道并恢复绑定和消费,支持当前节点故障时的快速恢复"""
|
|
372
496
|
try:
|
|
497
|
+
# 连接已关闭时触发完整重连(尝试其他节点)
|
|
373
498
|
if not self.connection or self.connection.is_closed:
|
|
499
|
+
logger.warning("连接已关闭,触发集群重连")
|
|
500
|
+
await self._schedule_reconnect()
|
|
374
501
|
return
|
|
375
502
|
|
|
376
503
|
# 重新创建通道
|
|
377
504
|
self.channel = await self.connection.channel()
|
|
378
|
-
await self.channel.set_qos(prefetch_count=
|
|
505
|
+
await self.channel.set_qos(prefetch_count=self.prefetch_count)
|
|
506
|
+
|
|
507
|
+
# 重新获取交换机
|
|
508
|
+
self.exchange = await self.channel.get_exchange(self.exchange_name)
|
|
379
509
|
|
|
380
510
|
# 重新绑定队列和交换机
|
|
381
|
-
if self.
|
|
382
|
-
await self.
|
|
511
|
+
if self.queue_name:
|
|
512
|
+
self.queue = await self.channel.get_queue(self.queue_name)
|
|
513
|
+
if self.queue and self.exchange:
|
|
514
|
+
await self._bind_queue()
|
|
383
515
|
|
|
384
516
|
# 重新开始消费
|
|
385
517
|
if self._is_consuming and self.message_handler:
|
|
386
518
|
await self.start_consuming()
|
|
387
519
|
|
|
388
|
-
|
|
520
|
+
logger.info(f"通道已重新创建并恢复服务 (主机: {self._active_host})")
|
|
389
521
|
self._update_activity_timestamp()
|
|
390
522
|
except Exception as e:
|
|
391
|
-
|
|
523
|
+
logger.error(f"通道重建失败,触发集群重连: {str(e)} (主机: {self._active_host})")
|
|
392
524
|
await self._schedule_reconnect()
|
|
393
525
|
|
|
394
|
-
def
|
|
395
|
-
"""
|
|
526
|
+
def _start_keepalive(self) -> None:
|
|
527
|
+
"""启动连接保活任务,维护集群连接心跳"""
|
|
396
528
|
if self._closed or (self._keepalive_task and not self._keepalive_task.done()):
|
|
397
529
|
return
|
|
398
530
|
|
|
399
|
-
async def
|
|
531
|
+
async def keepalive():
|
|
400
532
|
while not self._closed and self.is_connected:
|
|
401
533
|
current_time = asyncio.get_event_loop().time()
|
|
402
534
|
# 检查是否超过指定时间无活动
|
|
403
|
-
if current_time - self.
|
|
404
|
-
|
|
535
|
+
if current_time - self._last_activity_timestamp > self.heartbeat * 1.5:
|
|
536
|
+
logger.debug(
|
|
537
|
+
f"连接 {self.heartbeat*1.5}s 无活动,执行保活检查 (主机: {self._active_host})")
|
|
405
538
|
try:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
timeout=5
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
self._update_activity_timestamp()
|
|
539
|
+
if self.connection and self.connection.is_closed:
|
|
540
|
+
logger.warning("连接已关闭,触发集群重连")
|
|
541
|
+
await self._schedule_reconnect()
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# 执行轻量级操作保持连接活跃
|
|
545
|
+
if self.channel:
|
|
546
|
+
await asyncio.wait_for(
|
|
547
|
+
self.channel.declare_exchange(
|
|
548
|
+
name=self.exchange_name,
|
|
549
|
+
type=self.exchange_type,
|
|
550
|
+
passive=True # 仅检查存在性
|
|
551
|
+
),
|
|
552
|
+
timeout=5
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
self._update_activity_timestamp()
|
|
427
556
|
except asyncio.TimeoutError:
|
|
428
|
-
|
|
557
|
+
logger.warning(
|
|
558
|
+
f"保活检查超时,触发集群重连 (主机: {self._active_host})")
|
|
429
559
|
await self._schedule_reconnect()
|
|
430
560
|
except Exception as e:
|
|
431
|
-
|
|
561
|
+
logger.warning(
|
|
562
|
+
f"保活检查失败: {str(e)},触发集群重连 (主机: {self._active_host})")
|
|
432
563
|
await self._schedule_reconnect()
|
|
433
564
|
|
|
434
|
-
await asyncio.sleep(self.
|
|
565
|
+
await asyncio.sleep(self.heartbeat / 2) # 每心跳间隔的一半检查一次
|
|
435
566
|
|
|
436
|
-
self._keepalive_task = asyncio.create_task(
|
|
567
|
+
self._keepalive_task = asyncio.create_task(keepalive())
|
|
437
568
|
|
|
438
|
-
async def _schedule_reconnect(self):
|
|
439
|
-
"""
|
|
569
|
+
async def _schedule_reconnect(self) -> None:
|
|
570
|
+
"""安排重新连接(尝试集群中的所有节点)"""
|
|
440
571
|
if self._reconnect_task and not self._reconnect_task.done():
|
|
441
572
|
return
|
|
442
573
|
|
|
443
|
-
|
|
574
|
+
logger.info(f"将在 {self.reconnection_delay} 秒后尝试重新连接到RabbitMQ集群...")
|
|
444
575
|
|
|
445
|
-
async def
|
|
576
|
+
async def reconnect():
|
|
446
577
|
try:
|
|
447
578
|
await asyncio.sleep(self.reconnection_delay)
|
|
448
579
|
if not self._closed:
|
|
449
|
-
|
|
580
|
+
# 重连时尝试所有节点
|
|
581
|
+
await self.connect(force_reconnect=True)
|
|
450
582
|
except Exception as e:
|
|
451
|
-
|
|
452
|
-
# 如果重连失败,再次安排重连
|
|
583
|
+
logger.error(f"重连任务失败: {str(e)}")
|
|
453
584
|
if not self._closed:
|
|
454
585
|
await self._schedule_reconnect()
|
|
455
586
|
|
|
456
|
-
self._reconnect_task = asyncio.create_task(
|
|
587
|
+
self._reconnect_task = asyncio.create_task(reconnect())
|
|
457
588
|
|
|
458
589
|
async def close(self) -> None:
|
|
459
|
-
"""
|
|
590
|
+
"""关闭连接并清理资源"""
|
|
460
591
|
self._closed = True
|
|
461
592
|
self._is_consuming = False
|
|
462
593
|
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if self._consuming_task and not self._consuming_task.done():
|
|
473
|
-
self._consuming_task.cancel()
|
|
594
|
+
# 取消所有任务
|
|
595
|
+
for task in [self._keepalive_task, self._reconnect_task,
|
|
596
|
+
self._consuming_task, self._monitor_task]:
|
|
597
|
+
if task and not task.done():
|
|
598
|
+
task.cancel()
|
|
599
|
+
try:
|
|
600
|
+
await task
|
|
601
|
+
except asyncio.CancelledError:
|
|
602
|
+
pass
|
|
474
603
|
|
|
475
604
|
# 关闭连接
|
|
476
605
|
if self.connection and not self.connection.is_closed:
|
|
477
606
|
try:
|
|
478
607
|
await asyncio.wait_for(self.connection.close(), timeout=5)
|
|
479
608
|
except Exception as e:
|
|
480
|
-
|
|
609
|
+
logger.warning(f"关闭连接时出错 (主机: {self._active_host}): {str(e)}")
|
|
481
610
|
|
|
482
611
|
# 重置状态
|
|
483
612
|
self.connection = None
|
|
@@ -488,241 +617,263 @@ class RabbitMQClient:
|
|
|
488
617
|
self._queue_exists = False
|
|
489
618
|
self._queue_bound = False
|
|
490
619
|
self._consumer_tag = None
|
|
491
|
-
self.
|
|
492
|
-
self.
|
|
620
|
+
self._processing_message_ids.clear()
|
|
621
|
+
self._active_host = None
|
|
622
|
+
|
|
623
|
+
logger.info("RabbitMQ客户端已关闭")
|
|
493
624
|
|
|
494
|
-
async def
|
|
625
|
+
async def publish(
|
|
495
626
|
self,
|
|
496
627
|
message_body: Union[str, Dict[str, Any]],
|
|
628
|
+
routing_key: Optional[str] = None,
|
|
497
629
|
content_type: str = "application/json",
|
|
498
|
-
headers: Optional[Dict[str, Any]] = None
|
|
630
|
+
headers: Optional[Dict[str, Any]] = None,
|
|
631
|
+
delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT
|
|
499
632
|
) -> None:
|
|
500
|
-
"""
|
|
633
|
+
"""
|
|
634
|
+
发布消息到交换机(自动处理连接故障并重试)
|
|
635
|
+
|
|
636
|
+
:param message_body: 消息体,可以是字符串或字典
|
|
637
|
+
:param routing_key: 路由键,如未指定则使用实例的routing_key
|
|
638
|
+
:param content_type: 内容类型
|
|
639
|
+
:param headers: 消息头
|
|
640
|
+
:param delivery_mode: 投递模式,持久化或非持久化
|
|
641
|
+
"""
|
|
501
642
|
if not self.is_connected:
|
|
502
|
-
|
|
643
|
+
logger.warning("连接已关闭,尝试重连后发布消息")
|
|
503
644
|
await self.connect(force_reconnect=True)
|
|
504
645
|
|
|
505
646
|
if not self.channel or not self.exchange:
|
|
506
647
|
raise Exception("RabbitMQ连接未初始化")
|
|
507
648
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
649
|
+
# 处理消息体
|
|
650
|
+
if isinstance(message_body, dict):
|
|
651
|
+
message_body_str = json.dumps(message_body, ensure_ascii=False)
|
|
652
|
+
if content_type == "text/plain":
|
|
653
|
+
content_type = "application/json"
|
|
654
|
+
else:
|
|
655
|
+
message_body_str = str(message_body)
|
|
656
|
+
|
|
657
|
+
# 创建消息对象
|
|
658
|
+
message = Message(
|
|
659
|
+
body=message_body_str.encode(),
|
|
660
|
+
content_type=content_type,
|
|
661
|
+
headers=headers or {},
|
|
662
|
+
delivery_mode=delivery_mode
|
|
663
|
+
)
|
|
522
664
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
665
|
+
# 发布消息(带重试机制)
|
|
666
|
+
retry_count = 0
|
|
667
|
+
while retry_count < 2: # 最多重试2次
|
|
668
|
+
try:
|
|
669
|
+
await self.exchange.publish(
|
|
670
|
+
message,
|
|
671
|
+
routing_key=routing_key or self.routing_key or '#'
|
|
672
|
+
)
|
|
673
|
+
self._update_activity_timestamp()
|
|
674
|
+
logger.debug(
|
|
675
|
+
f"消息已发布到交换机 '{self.exchange_name}' (主机: {self._active_host})")
|
|
676
|
+
return
|
|
677
|
+
except (ConnectionClosed, ChannelInvalidStateError):
|
|
678
|
+
retry_count += 1
|
|
679
|
+
logger.warning(f"连接已关闭,尝试重连后重新发布 (重试次数: {retry_count})")
|
|
680
|
+
await self.connect(force_reconnect=True)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
retry_count += 1
|
|
683
|
+
logger.error(f"消息发布失败 (重试次数: {retry_count}): {str(e)}")
|
|
684
|
+
if retry_count < 2:
|
|
685
|
+
await asyncio.sleep(1)
|
|
686
|
+
|
|
687
|
+
raise Exception(f"消息发布失败,经过{retry_count}次重试仍未成功")
|
|
538
688
|
|
|
539
689
|
def set_message_handler(
|
|
540
690
|
self,
|
|
541
691
|
handler: Callable[
|
|
542
|
-
[Union[
|
|
543
|
-
Coroutine
|
|
692
|
+
[Union[Dict[str, Any], str], AbstractIncomingMessage],
|
|
693
|
+
Coroutine[Any, Any, None]
|
|
544
694
|
]
|
|
545
695
|
) -> None:
|
|
546
|
-
"""
|
|
547
|
-
|
|
696
|
+
"""
|
|
697
|
+
设置消息处理函数
|
|
548
698
|
|
|
549
|
-
|
|
550
|
-
"""
|
|
551
|
-
|
|
552
|
-
logging.debug("已经在消费中,返回现有consumer_tag")
|
|
553
|
-
return self._consumer_tag
|
|
699
|
+
:param handler: 消息处理函数,接收解析后的消息和原始消息对象
|
|
700
|
+
"""
|
|
701
|
+
self.message_handler = handler
|
|
554
702
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
while attempt < max_attempts:
|
|
559
|
-
if not self.is_connected:
|
|
560
|
-
await self.connect()
|
|
703
|
+
async def start_consuming(self) -> ConsumerTag:
|
|
704
|
+
"""
|
|
705
|
+
开始消费消息
|
|
561
706
|
|
|
562
|
-
|
|
563
|
-
|
|
707
|
+
:return: 消费者标签
|
|
708
|
+
"""
|
|
709
|
+
if self._is_consuming:
|
|
710
|
+
logger.debug("已经在消费中,返回现有consumer_tag")
|
|
711
|
+
if self._consumer_tag:
|
|
712
|
+
return self._consumer_tag
|
|
713
|
+
raise Exception("消费已启动但未获取到consumer_tag")
|
|
564
714
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
await
|
|
715
|
+
# 确保连接和队列已准备好
|
|
716
|
+
if not self.is_connected:
|
|
717
|
+
await self.connect()
|
|
568
718
|
|
|
569
719
|
if not self.queue:
|
|
570
|
-
|
|
571
|
-
logging.warning("最后尝试重新连接并声明队列")
|
|
572
|
-
await self.connect(force_reconnect=True, declare_queue=True)
|
|
573
|
-
if not self.queue:
|
|
574
|
-
raise Exception("队列未初始化,多次尝试后仍无法创建")
|
|
720
|
+
raise Exception("队列未初始化,无法开始消费")
|
|
575
721
|
|
|
576
722
|
if not self.message_handler:
|
|
577
723
|
raise Exception("未设置消息处理函数")
|
|
578
724
|
|
|
579
725
|
self._is_consuming = True
|
|
726
|
+
logger.info(
|
|
727
|
+
f"开始消费队列: {self.actual_queue_name} (主机: {self._active_host})")
|
|
580
728
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
logging.info(f"消费者已启动,tag: {self._consumer_tag}")
|
|
588
|
-
|
|
589
|
-
# 保持消费循环
|
|
590
|
-
while self._is_consuming and self.is_connected:
|
|
591
|
-
await asyncio.sleep(1)
|
|
592
|
-
|
|
593
|
-
# 如果退出循环,取消消费(增加重试逻辑)
|
|
594
|
-
if self._consumer_tag and self.queue and not self.queue.channel.is_closed:
|
|
595
|
-
await self._safe_cancel_consumer()
|
|
729
|
+
try:
|
|
730
|
+
# 开始消费,使用aio-pika的队列消费方法
|
|
731
|
+
self._consumer_tag = await self.queue.consume(
|
|
732
|
+
self._message_wrapper,
|
|
733
|
+
no_ack=False # 手动确认消息
|
|
734
|
+
)
|
|
596
735
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
736
|
+
logger.info(
|
|
737
|
+
f"消费者已启动,队列: {self.actual_queue_name}, tag: {self._consumer_tag}, 主机: {self._active_host}")
|
|
738
|
+
return self._consumer_tag
|
|
739
|
+
except Exception as e:
|
|
740
|
+
self._is_consuming = False
|
|
741
|
+
logger.error(
|
|
742
|
+
f"启动消费失败: {str(e)} (主机: {self._active_host})", exc_info=True)
|
|
743
|
+
raise
|
|
600
744
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if self._closed or not self._is_consuming:
|
|
606
|
-
break
|
|
745
|
+
async def _safe_cancel_consumer(self) -> bool:
|
|
746
|
+
"""安全取消消费者"""
|
|
747
|
+
if not self._consumer_tag or not self.queue or not self.channel:
|
|
748
|
+
return True
|
|
607
749
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
await asyncio.sleep(1)
|
|
616
|
-
except asyncio.CancelledError:
|
|
617
|
-
logging.info("消费任务已取消")
|
|
618
|
-
except Exception as e:
|
|
619
|
-
logging.error(f"消费任务出错: {str(e)}", exc_info=True)
|
|
620
|
-
finally:
|
|
621
|
-
self._is_consuming = False
|
|
622
|
-
self._consumer_tag = None
|
|
623
|
-
logging.info("消费任务已结束")
|
|
624
|
-
|
|
625
|
-
# 保存消费任务引用
|
|
626
|
-
self._consuming_task = asyncio.create_task(consume_task())
|
|
627
|
-
return self._consumer_tag
|
|
628
|
-
|
|
629
|
-
async def _safe_cancel_consumer(self, max_retries: int = 3) -> bool:
|
|
630
|
-
"""安全取消消费者,增加重试机制"""
|
|
631
|
-
if not self._consumer_tag or not self.queue:
|
|
750
|
+
try:
|
|
751
|
+
await asyncio.wait_for(
|
|
752
|
+
self.queue.cancel(self._consumer_tag),
|
|
753
|
+
timeout=self.rpc_timeout
|
|
754
|
+
)
|
|
755
|
+
logger.info(
|
|
756
|
+
f"消费者 {self._consumer_tag} 已取消 (主机: {self._active_host})")
|
|
632
757
|
return True
|
|
758
|
+
except (ChannelInvalidStateError, ConnectionClosed):
|
|
759
|
+
logger.warning(f"取消消费者失败:通道或连接已关闭 (主机: {self._active_host})")
|
|
760
|
+
return False
|
|
761
|
+
except asyncio.TimeoutError:
|
|
762
|
+
logger.warning(f"取消消费者超时 (主机: {self._active_host})")
|
|
763
|
+
return False
|
|
764
|
+
except Exception as e:
|
|
765
|
+
logger.error(f"取消消费者异常: {str(e)} (主机: {self._active_host})")
|
|
766
|
+
return False
|
|
633
767
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
timeout=self.rpc_timeout
|
|
639
|
-
)
|
|
640
|
-
logging.info(f"消费者 {self._consumer_tag} 已取消")
|
|
641
|
-
return True
|
|
642
|
-
except ChannelInvalidStateError:
|
|
643
|
-
if attempt >= max_retries - 1:
|
|
644
|
-
logging.error(f"取消消费者 {self._consumer_tag} 失败:通道已关闭")
|
|
645
|
-
return False
|
|
646
|
-
logging.warning(f"取消消费者尝试 {attempt+1} 失败,通道状态异常,重试中...")
|
|
647
|
-
await asyncio.sleep(1)
|
|
648
|
-
except asyncio.TimeoutError:
|
|
649
|
-
if attempt >= max_retries - 1:
|
|
650
|
-
logging.error(f"取消消费者 {self._consumer_tag} 超时")
|
|
651
|
-
return False
|
|
652
|
-
logging.warning(f"取消消费者尝试 {attempt+1} 超时,重试中...")
|
|
653
|
-
await asyncio.sleep(1)
|
|
654
|
-
except Exception as e:
|
|
655
|
-
logging.error(f"取消消费者异常: {str(e)}")
|
|
656
|
-
return False
|
|
657
|
-
return False
|
|
768
|
+
async def stop_consuming(self) -> None:
|
|
769
|
+
"""停止消费消息,等待正在处理的消息完成"""
|
|
770
|
+
if not self._is_consuming:
|
|
771
|
+
return
|
|
658
772
|
|
|
659
|
-
async def stop_consuming(self, timeout: float = 5.0) -> None:
|
|
660
|
-
"""停止消费消息,延长超时时间并增加重试"""
|
|
661
773
|
self._is_consuming = False
|
|
662
774
|
|
|
663
|
-
|
|
775
|
+
# 取消消费者,停止接收新消息
|
|
776
|
+
if self._consumer_tag and self.queue:
|
|
664
777
|
await self._safe_cancel_consumer()
|
|
665
778
|
|
|
666
|
-
#
|
|
667
|
-
if self.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
779
|
+
# 等待所有正在处理的消息完成
|
|
780
|
+
if self._processing_message_ids:
|
|
781
|
+
logger.info(
|
|
782
|
+
f"等待 {len(self._processing_message_ids)} 个正在处理的消息完成... (主机: {self._active_host})"
|
|
783
|
+
)
|
|
784
|
+
# 循环等待直到所有消息处理完成
|
|
785
|
+
while self._processing_message_ids and not self._closed:
|
|
786
|
+
await asyncio.sleep(0.1)
|
|
787
|
+
|
|
788
|
+
# 清理状态
|
|
789
|
+
self._consumer_tag = None
|
|
790
|
+
self._processing_message_ids.clear()
|
|
791
|
+
|
|
792
|
+
logger.info(
|
|
793
|
+
f"已停止消费队列: {self.actual_queue_name} (主机: {self._active_host})")
|
|
675
794
|
|
|
676
795
|
async def _parse_message(self, message: AbstractIncomingMessage) -> Union[Dict[str, Any], str]:
|
|
677
|
-
"""
|
|
796
|
+
"""解析消息体"""
|
|
678
797
|
try:
|
|
679
798
|
body_str = message.body.decode('utf-8')
|
|
680
|
-
self._update_activity_timestamp()
|
|
799
|
+
self._update_activity_timestamp()
|
|
681
800
|
|
|
682
801
|
if self.auto_parse_json:
|
|
683
802
|
return json.loads(body_str)
|
|
684
803
|
return body_str
|
|
685
804
|
except json.JSONDecodeError:
|
|
686
|
-
|
|
805
|
+
logger.warning(f"消息解析JSON失败,返回原始字符串 (主机: {self._active_host})")
|
|
687
806
|
return body_str
|
|
688
807
|
except Exception as e:
|
|
689
|
-
|
|
808
|
+
logger.error(f"消息解析出错: {str(e)} (主机: {self._active_host})")
|
|
690
809
|
return message.body.decode('utf-8')
|
|
691
810
|
|
|
692
811
|
async def _message_wrapper(self, message: AbstractIncomingMessage) -> None:
|
|
812
|
+
"""消息处理包装器,处理消息接收、解析、分发和确认"""
|
|
693
813
|
if not self.message_handler or not self._is_consuming:
|
|
694
|
-
|
|
695
|
-
|
|
814
|
+
logger.warning("未设置消息处理器或已停止消费,确认消息")
|
|
815
|
+
await message.ack()
|
|
816
|
+
return
|
|
817
|
+
|
|
818
|
+
# 跟踪消息ID,防止重复处理
|
|
819
|
+
message_id = message.message_id or str(id(message))
|
|
820
|
+
if message_id in self._processing_message_ids:
|
|
821
|
+
logger.warning(
|
|
822
|
+
f"检测到重复处理的消息ID: {message_id},直接确认 (主机: {self._active_host})")
|
|
823
|
+
await message.ack()
|
|
696
824
|
return
|
|
697
825
|
|
|
826
|
+
self._processing_message_ids.add(message_id)
|
|
827
|
+
|
|
698
828
|
try:
|
|
829
|
+
logger.debug(
|
|
830
|
+
f"收到队列 {self.actual_queue_name} 的消息: {message_id} (主机: {self._active_host})")
|
|
831
|
+
|
|
832
|
+
# 解析消息
|
|
699
833
|
parsed_data = await self._parse_message(message)
|
|
700
|
-
|
|
834
|
+
|
|
835
|
+
# 消息处理超时控制
|
|
836
|
+
# try:
|
|
837
|
+
# await asyncio.wait_for(
|
|
838
|
+
# self.message_handler(MQMsgModel(**parsed_data), message),
|
|
839
|
+
# timeout=self.message_process_timeout
|
|
840
|
+
# )
|
|
841
|
+
# except asyncio.TimeoutError:
|
|
842
|
+
# logger.error(
|
|
843
|
+
# f"消息 {message_id} 处理超时(超过 {self.message_process_timeout} 秒)(主机: {self._active_host})")
|
|
844
|
+
# await message.ack() # 超时也确认,避免无限重试
|
|
845
|
+
# return
|
|
846
|
+
await self.message_handler(MQMsgModel(**parsed_data), message)
|
|
847
|
+
|
|
848
|
+
# 处理成功,确认消息
|
|
701
849
|
await message.ack()
|
|
702
850
|
self._update_activity_timestamp()
|
|
851
|
+
self._update_message_processed_timestamp()
|
|
852
|
+
logger.debug(f"消息 {message_id} 处理完成并确认 (主机: {self._active_host})")
|
|
853
|
+
|
|
703
854
|
except Exception as e:
|
|
855
|
+
# 处理失败,根据重试次数决定是否重新发布
|
|
704
856
|
current_headers = message.headers or {}
|
|
705
857
|
retry_count = current_headers.get('x-retry-count', 0)
|
|
706
858
|
retry_count += 1
|
|
707
859
|
|
|
708
|
-
|
|
709
|
-
f"
|
|
860
|
+
logger.error(
|
|
861
|
+
f"消息 {message_id} 处理出错(第{retry_count}次重试): {str(e)} (主机: {self._active_host})",
|
|
710
862
|
exc_info=True
|
|
711
863
|
)
|
|
712
864
|
|
|
713
|
-
# 判断是否超过最大重试次数
|
|
714
865
|
if retry_count >= MAX_RETRY_COUNT:
|
|
715
|
-
|
|
716
|
-
f"
|
|
866
|
+
logger.error(
|
|
867
|
+
f"消息 {message_id} 已达到最大重试次数({MAX_RETRY_COUNT}次),标记为失败 (主机: {self._active_host})")
|
|
717
868
|
await message.ack()
|
|
718
869
|
self._update_activity_timestamp()
|
|
719
870
|
return
|
|
720
871
|
|
|
721
|
-
#
|
|
872
|
+
# 准备重新发布的消息
|
|
722
873
|
new_headers = current_headers.copy()
|
|
723
874
|
new_headers['x-retry-count'] = retry_count
|
|
724
875
|
|
|
725
|
-
new_message =
|
|
876
|
+
new_message = Message(
|
|
726
877
|
body=message.body,
|
|
727
878
|
content_type=message.content_type,
|
|
728
879
|
headers=new_headers,
|
|
@@ -732,14 +883,19 @@ class RabbitMQClient:
|
|
|
732
883
|
# 拒绝原消息(不重新入队)
|
|
733
884
|
await message.reject(requeue=False)
|
|
734
885
|
|
|
735
|
-
#
|
|
886
|
+
# 重新发布消息
|
|
736
887
|
if self.exchange:
|
|
737
888
|
await self.exchange.publish(
|
|
738
889
|
new_message,
|
|
739
890
|
routing_key=self.routing_key or '#'
|
|
740
891
|
)
|
|
741
892
|
self._update_activity_timestamp()
|
|
742
|
-
|
|
893
|
+
logger.info(
|
|
894
|
+
f"消息 {message_id} 已重新发布,当前重试次数: {retry_count} (主机: {self._active_host})")
|
|
895
|
+
finally:
|
|
896
|
+
# 移除消息ID跟踪
|
|
897
|
+
if message_id in self._processing_message_ids:
|
|
898
|
+
self._processing_message_ids.remove(message_id)
|
|
743
899
|
|
|
744
900
|
async def __aenter__(self):
|
|
745
901
|
await self.connect()
|