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