sycommon-python-lib 0.1.56__py3-none-any.whl → 0.1.56b2__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 (40) hide show
  1. sycommon/config/Config.py +3 -24
  2. sycommon/llm/embedding.py +23 -78
  3. sycommon/llm/get_llm.py +164 -24
  4. sycommon/logging/kafka_log.py +433 -187
  5. sycommon/middleware/exception.py +16 -10
  6. sycommon/middleware/timeout.py +1 -2
  7. sycommon/middleware/traceid.py +76 -81
  8. sycommon/rabbitmq/rabbitmq_client.py +242 -232
  9. sycommon/rabbitmq/rabbitmq_pool.py +218 -278
  10. sycommon/rabbitmq/rabbitmq_service.py +843 -25
  11. sycommon/services.py +96 -122
  12. sycommon/synacos/nacos_service.py +779 -63
  13. sycommon/tools/merge_headers.py +0 -20
  14. sycommon/tools/snowflake.py +153 -101
  15. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/METADATA +8 -10
  16. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/RECORD +19 -40
  17. sycommon/config/LangfuseConfig.py +0 -15
  18. sycommon/config/SentryConfig.py +0 -13
  19. sycommon/llm/llm_tokens.py +0 -119
  20. sycommon/llm/struct_token.py +0 -192
  21. sycommon/llm/sy_langfuse.py +0 -103
  22. sycommon/llm/usage_token.py +0 -117
  23. sycommon/notice/__init__.py +0 -0
  24. sycommon/notice/uvicorn_monitor.py +0 -200
  25. sycommon/rabbitmq/rabbitmq_service_client_manager.py +0 -206
  26. sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +0 -73
  27. sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +0 -285
  28. sycommon/rabbitmq/rabbitmq_service_core.py +0 -117
  29. sycommon/rabbitmq/rabbitmq_service_producer_manager.py +0 -238
  30. sycommon/sentry/__init__.py +0 -0
  31. sycommon/sentry/sy_sentry.py +0 -35
  32. sycommon/synacos/nacos_client_base.py +0 -119
  33. sycommon/synacos/nacos_config_manager.py +0 -107
  34. sycommon/synacos/nacos_heartbeat_manager.py +0 -144
  35. sycommon/synacos/nacos_service_discovery.py +0 -157
  36. sycommon/synacos/nacos_service_registration.py +0 -270
  37. sycommon/tools/env.py +0 -62
  38. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/WHEEL +0 -0
  39. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/entry_points.txt +0 -0
  40. {sycommon_python_lib-0.1.56.dist-info → sycommon_python_lib-0.1.56b2.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
+ from aio_pika import Channel
2
+ from typing import Optional
1
3
  import asyncio
2
4
  import json
3
- from typing import Optional, Callable, Coroutine, Dict, Any, Union
4
- from aio_pika import Channel, Message, DeliveryMode, ExchangeType
5
+ from typing import Callable, Coroutine, Dict, Any, Union
6
+ from aio_pika import Message, DeliveryMode, ExchangeType
5
7
  from aio_pika.abc import (
6
8
  AbstractExchange,
7
9
  AbstractQueue,
@@ -18,7 +20,13 @@ logger = SYLogger
18
20
 
19
21
  class RabbitMQClient:
20
22
  """
21
- RabbitMQ 客户端
23
+ RabbitMQ 客户端(支持消息发布、消费、自动重连、异常重试)
24
+ 核心特性:
25
+ 1. 基于单通道连接池复用资源,性能优化
26
+ 2. 依赖连接池原生自动重连,客户端仅重建自身资源
27
+ 3. 消息发布支持重试+mandatory机制+超时控制,确保路由有效
28
+ 4. 消费支持手动ACK/NACK
29
+ 5. 兼容JSON/字符串/字典消息格式
22
30
  """
23
31
 
24
32
  def __init__(
@@ -34,10 +42,12 @@ class RabbitMQClient:
34
42
  create_if_not_exists: bool = True,
35
43
  **kwargs,
36
44
  ):
45
+ # 依赖注入:连接池(必须已初始化)
37
46
  self.connection_pool = connection_pool
38
47
  if not self.connection_pool._initialized:
39
48
  raise RuntimeError("连接池未初始化,请先调用 connection_pool.init_pools()")
40
49
 
50
+ # 交换机配置
41
51
  self.exchange_name = exchange_name.strip()
42
52
  try:
43
53
  self.exchange_type = ExchangeType(exchange_type.lower())
@@ -45,16 +55,17 @@ class RabbitMQClient:
45
55
  logger.warning(f"无效的exchange_type: {exchange_type},默认使用'topic'")
46
56
  self.exchange_type = ExchangeType.TOPIC
47
57
 
58
+ # 队列配置
48
59
  self.queue_name = queue_name.strip() if queue_name else None
49
60
  self.routing_key = routing_key.strip() if routing_key else "#"
50
- self.durable = durable
51
- self.auto_delete = auto_delete
52
- self.auto_parse_json = auto_parse_json
53
- self.create_if_not_exists = create_if_not_exists
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 # 不存在则创建交换机/队列
54
65
 
55
- # 资源状态
66
+ # 内部状态(资源+连接)
56
67
  self._channel: Optional[Channel] = None
57
- self._channel_conn: Optional[AbstractRobustConnection] = None
68
+ self._channel_conn: Optional[AbstractRobustConnection] = None # 通道所属连接
58
69
  self._exchange: Optional[AbstractExchange] = None
59
70
  self._queue: Optional[AbstractQueue] = None
60
71
  self._consumer_tag: Optional[ConsumerTag] = None
@@ -62,38 +73,45 @@ class RabbitMQClient:
62
73
  MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]]] = None
63
74
  self._closed = False
64
75
 
65
- # 并发控制
76
+ # 线程安全锁
66
77
  self._consume_lock = asyncio.Lock()
67
78
  self._connect_lock = asyncio.Lock()
68
-
69
- # 防止并发重连覆盖
70
- self._connecting = False
71
- self._connect_condition = asyncio.Condition()
72
-
79
+ # 跟踪连接关闭回调(用于后续移除)
73
80
  self._conn_close_callback: Optional[Callable] = None
81
+ # 控制重连频率的信号量(限制并发重连数)
74
82
  self._reconnect_semaphore = asyncio.Semaphore(1)
75
- self._current_reconnect_task: Optional[asyncio.Task] = None
83
+ # 固定重连间隔15秒(全局统一)
76
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
77
91
 
78
92
  @property
79
93
  async def is_connected(self) -> bool:
94
+ """异步检查客户端连接状态(属性,不可调用)"""
80
95
  if self._closed:
81
96
  return False
82
97
  try:
98
+ # 单通道场景:校验通道+连接+核心资源都有效
83
99
  return (
84
100
  self._channel and not self._channel.is_closed
85
101
  and self._channel_conn and not self._channel_conn.is_closed
86
102
  and self._exchange is not None
87
103
  and (not self.queue_name or self._queue is not None)
88
104
  )
89
- except Exception:
105
+ except Exception as e:
106
+ logger.warning(f"检查连接状态失败: {str(e)}")
90
107
  return False
91
108
 
92
109
  async def _rebuild_resources(self) -> None:
110
+ """重建交换机/队列等资源(依赖已有的通道)"""
93
111
  if not self._channel or self._channel.is_closed:
94
112
  raise RuntimeError("无有效通道,无法重建资源")
95
113
 
96
- # 声明交换机
114
+ # 1. 声明交换机
97
115
  self._exchange = await self._channel.declare_exchange(
98
116
  name=self.exchange_name,
99
117
  type=self.exchange_type,
@@ -101,9 +119,10 @@ class RabbitMQClient:
101
119
  auto_delete=self.auto_delete,
102
120
  passive=not self.create_if_not_exists,
103
121
  )
104
- logger.info(f"交换机重建成功: {self.exchange_name}")
122
+ logger.info(
123
+ f"交换机重建成功: {self.exchange_name}(类型: {self.exchange_type.value})")
105
124
 
106
- # 声明队列
125
+ # 2. 声明队列(如果配置了队列名)
107
126
  if self.queue_name:
108
127
  self._queue = await self._channel.declare_queue(
109
128
  name=self.queue_name,
@@ -111,134 +130,93 @@ class RabbitMQClient:
111
130
  auto_delete=self.auto_delete,
112
131
  passive=not self.create_if_not_exists,
113
132
  )
114
- await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
115
- logger.info(f"队列重建成功: {self.queue_name}")
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})"
141
+ )
116
142
 
117
143
  async def connect(self) -> None:
118
144
  if self._closed:
119
145
  raise RuntimeError("客户端已关闭,无法重新连接")
120
146
 
121
- # 1. 并发控制:使用 _connect_lock 保证只有一个协程在执行连接流程
122
147
  async with self._connect_lock:
123
- # 如果已经在连了,等待其完成
124
- if self._connecting:
125
- logger.debug("连接正在进行中,等待现有连接完成...")
126
- try:
127
- # 等待条件变量,超时设为 60 秒防止死等
128
- await asyncio.wait_for(
129
- self._connect_condition.wait_for(
130
- lambda: not self._connecting),
131
- timeout=60.0
132
- )
133
- except asyncio.TimeoutError:
134
- raise RuntimeError("等待连接超时")
135
-
136
- # 等待结束后,再次检查状态
137
- if not await self.is_connected:
138
- raise RuntimeError("等待重连后,连接状态依然无效")
139
- return
140
-
141
- # 标记开始连接
142
- self._connecting = True
143
-
144
- # 释放 _connect_lock,允许其他协程读取状态,但在连接完成前阻止新的连接请求
145
- # 注意:这里释放了 _connect_lock,但 self._connecting = True 阻止了新的连接流程
146
-
147
- try:
148
- # --- 阶段1: 清理旧资源 ---
149
- # 重新获取锁进行资源清理
150
- async with self._connect_lock:
151
- was_consuming = self._consumer_tag is not None
152
-
153
- if self._channel_conn and self._conn_close_callback:
154
- try:
155
- self._channel_conn.close_callbacks.discard(
156
- self._conn_close_callback)
157
- except Exception:
158
- pass
159
-
160
- self._channel = None
161
- self._channel_conn = None
162
- self._exchange = None
163
- self._queue = None
164
- self._conn_close_callback = None
165
-
166
- # --- 阶段2: 获取新连接 (耗时IO) ---
167
- self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
168
-
169
- # 设置回调
170
- def on_conn_closed(conn, exc):
171
- logger.warning(f"检测到连接关闭: {exc}")
172
- if not self._closed and not self._connecting:
173
- asyncio.create_task(self._safe_reconnect())
174
-
175
- self._conn_close_callback = on_conn_closed
176
- if self._channel_conn:
177
- self._channel_conn.close_callbacks.add(
148
+ # 释放旧资源(回调+通道,单通道无需归还,仅清理状态)
149
+ if self._conn_close_callback and self._channel_conn:
150
+ self._channel_conn.close_callbacks.discard(
178
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
179
157
 
180
- # 重建资源
181
- await self._rebuild_resources()
182
-
183
- # --- 阶段3: 恢复消费 ---
184
- if was_consuming and self._message_handler and self.queue_name:
185
- logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复...")
186
- try:
187
- self._queue = await self._channel.declare_queue(
188
- name=self.queue_name,
189
- durable=self.durable,
190
- auto_delete=self.auto_delete,
191
- passive=False,
192
- )
193
- await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
194
- self._consumer_tag = await self._queue.consume(self._process_message_callback)
195
- logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
196
- except Exception as e:
197
- logger.error(f"❌ 自动恢复消费失败: {e}")
198
- self._consumer_tag = None
199
- else:
200
- self._consumer_tag = None
201
-
202
- logger.info("客户端连接初始化完成")
158
+ try:
159
+ # 从单通道池获取通道+连接(连接池自动确保通道有效)
160
+ self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
161
+
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)
203
178
 
204
- except Exception as e:
205
- logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
179
+ # 重建交换机/队列资源
180
+ await self._rebuild_resources()
206
181
 
207
- # 异常时清理资源
208
- async with self._connect_lock:
209
- if self._channel_conn and self._conn_close_callback:
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:
210
189
  self._channel_conn.close_callbacks.discard(
211
190
  self._conn_close_callback)
212
191
  self._channel = None
213
192
  self._channel_conn = None
214
- self._consumer_tag = None
215
-
216
- raise
217
-
218
- finally:
219
- # 【关键修复】必须在持有 Condition 内部锁的情况下调用 notify_all
220
- # 这里使用 async with self._connect_condition: 自动完成 acquire() ... notify_all() ... release()
221
- async with self._connect_condition:
222
- self._connecting = False
223
- self._connect_condition.notify_all()
193
+ # 触发重连
194
+ if not self._closed:
195
+ asyncio.create_task(self._safe_reconnect())
196
+ raise
224
197
 
225
198
  async def _safe_reconnect(self):
226
- """安全重连任务(仅用于被动监听连接关闭)"""
199
+ """安全重连:信号量控制并发+固定15秒间隔"""
227
200
  async with self._reconnect_semaphore:
228
- if self._closed:
201
+ # 检查是否已有重连任务在运行
202
+ if self._current_reconnect_task and not self._current_reconnect_task.done():
203
+ logger.debug("已有重连任务在运行,跳过重复触发")
229
204
  return
230
205
 
231
- # 如果已经在重连,直接忽略
232
- if self._connecting:
206
+ if self._closed or await self.is_connected:
207
+ logger.debug("客户端已关闭或已连接,取消重连")
233
208
  return
234
209
 
210
+ # 固定15秒重连间隔
235
211
  logger.info(f"将在{self._RECONNECT_INTERVAL}秒后尝试重连...")
236
212
  await asyncio.sleep(self._RECONNECT_INTERVAL)
237
213
 
238
214
  if self._closed or await self.is_connected:
215
+ logger.debug("重连等待期间客户端状态变化,取消重连")
239
216
  return
240
217
 
241
218
  try:
219
+ logger.info("开始重连RabbitMQ客户端...")
242
220
  self._current_reconnect_task = asyncio.create_task(
243
221
  self.connect())
244
222
  await self._current_reconnect_task
@@ -247,111 +225,113 @@ class RabbitMQClient:
247
225
  finally:
248
226
  self._current_reconnect_task = None
249
227
 
250
- async def set_message_handler(self, handler: Callable[..., Coroutine]) -> None:
228
+ async def set_message_handler(
229
+ self,
230
+ handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]],
231
+ ) -> None:
232
+ """设置消息处理器(必须是协程函数)"""
251
233
  if not asyncio.iscoroutinefunction(handler):
252
- raise TypeError("消息处理器必须是协程函数")
234
+ raise TypeError("消息处理器必须是协程函数(使用 async def 定义)")
235
+
253
236
  async with self._consume_lock:
254
237
  self._message_handler = handler
255
-
256
- async def _process_message_callback(self, message: AbstractIncomingMessage):
257
- try:
258
- msg_obj: MQMsgModel
259
- if self.auto_parse_json:
260
- try:
261
- body_dict = json.loads(message.body.decode("utf-8"))
262
- msg_obj = MQMsgModel(**body_dict)
263
- except json.JSONDecodeError as e:
264
- logger.error(f"JSON解析失败: {e}")
265
- await message.nack(requeue=False)
266
- return
267
- else:
268
- msg_obj = MQMsgModel(
269
- body=message.body.decode("utf-8"),
270
- routing_key=message.routing_key,
271
- delivery_tag=message.delivery_tag,
272
- traceId=message.headers.get("trace-id"),
273
- )
274
-
275
- SYLogger.set_trace_id(msg_obj.traceId)
276
-
277
- if self._message_handler:
278
- await self._message_handler(msg_obj, message)
279
-
280
- await message.ack()
281
-
282
- except Exception as e:
283
- logger.error(f"消息处理异常: {e}", exc_info=True)
284
- headers = dict(message.headers) if message.headers else {}
285
- current_retry = int(headers.get("x-retry-count", 0))
286
-
287
- if current_retry >= 3:
288
- logger.warning(f"重试次数超限,丢弃消息: {message.delivery_tag}")
289
- await message.reject(requeue=False)
290
- else:
291
- headers["x-retry-count"] = current_retry + 1
292
- try:
293
- new_msg = Message(
294
- body=message.body,
295
- headers=headers,
296
- content_type=message.content_type,
297
- delivery_mode=message.delivery_mode
298
- )
299
- # 这里的 publish 如果失败,会触发重连机制
300
- # 但注意,当前是在回调线程中,建议做好异常捕获
301
- await self._exchange.publish(new_msg, routing_key=message.routing_key)
302
- await message.ack()
303
- except Exception as pub_err:
304
- logger.error(f"重试发布失败: {pub_err}")
305
- await message.reject(requeue=False)
238
+ logger.info("消息处理器设置成功")
306
239
 
307
240
  async def start_consuming(self) -> Optional[ConsumerTag]:
241
+ """启动消息消费(支持自动重连)"""
308
242
  if self._closed:
309
243
  raise RuntimeError("客户端已关闭,无法启动消费")
310
244
 
311
245
  async with self._consume_lock:
246
+ # 1. 校验前置条件
312
247
  if not self._message_handler:
313
- raise RuntimeError("未设置消息处理器")
314
-
248
+ raise RuntimeError("未设置消息处理器,请先调用 set_message_handler()")
315
249
  if not await self.is_connected:
316
250
  await self.connect()
317
-
318
251
  if not self._queue:
319
- if self.queue_name:
320
- self._queue = await self._channel.declare_queue(
321
- name=self.queue_name,
322
- durable=self.durable,
323
- auto_delete=self.auto_delete,
324
- passive=not self.create_if_not_exists,
325
- )
326
- await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
327
- else:
328
- raise RuntimeError("未配置队列名")
252
+ raise RuntimeError("未配置队列名或队列未创建,无法启动消费")
329
253
 
330
- self._consumer_tag = await self._queue.consume(self._process_message_callback)
254
+ # 2. 定义消费回调(包含异常处理和重连逻辑)
255
+ async def consume_callback(message: AbstractIncomingMessage):
256
+ try:
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
268
+ else:
269
+ msg_obj = MQMsgModel(
270
+ body=message.body.decode("utf-8"),
271
+ routing_key=message.routing_key,
272
+ delivery_tag=message.delivery_tag,
273
+ )
274
+
275
+ # 调用消息处理器
276
+ await self._message_handler(msg_obj, message)
277
+
278
+ # 手动ACK
279
+ await message.ack()
280
+ logger.debug(
281
+ f"消息处理成功,delivery_tag: {message.delivery_tag}")
282
+
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:
290
+ logger.warning(
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)
296
+
297
+ # 连接失效则触发重连
298
+ if not await self.is_connected:
299
+ logger.warning("连接已失效,触发客户端重连")
300
+ asyncio.create_task(self._safe_reconnect())
301
+
302
+ # 3. 启动消费(单通道消费,避免阻塞发布需确保业务回调非阻塞)
303
+ self._consumer_tag = await self._queue.consume(consume_callback)
331
304
  logger.info(
332
- f"开始消费队列: {self._queue.name},tag: {self._consumer_tag}")
305
+ f"开始消费队列: {self._queue.name},consumer_tag: {self._consumer_tag}"
306
+ )
333
307
  return self._consumer_tag
334
308
 
335
309
  async def stop_consuming(self) -> None:
310
+ """停止消息消费(适配 RobustChannel)"""
336
311
  async with self._consume_lock:
337
- if self._consumer_tag and self._queue and self._channel:
338
- try:
312
+ try:
313
+ # 校验核心条件:消费标签、队列、通道均有效
314
+ if self._consumer_tag and self._queue and self._channel and not self._channel.is_closed:
315
+ # 使用队列的 cancel 方法(适配 RobustChannel)
339
316
  await self._queue.cancel(self._consumer_tag)
340
- logger.info(f"停止消费成功: {self._consumer_tag}")
341
- except Exception as e:
342
- logger.warning(f"停止消费异常: {e}")
343
- self._consumer_tag = None
344
-
345
- async def _handle_publish_failure(self):
346
- try:
347
- logger.info("检测到发布异常,强制连接池切换节点...")
348
- await self.connection_pool.force_reconnect()
349
- # 连接池切换后,必须刷新客户端资源
350
- await self.connect()
351
- logger.info("故障转移完成,资源已刷新")
352
- except Exception as e:
353
- logger.error(f"故障转移失败: {e}")
354
- raise
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
+ )
329
+ except Exception as e:
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 # 无论成败,清理消费标签
355
335
 
356
336
  async def publish(
357
337
  self,
@@ -361,9 +341,18 @@ class RabbitMQClient:
361
341
  delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT,
362
342
  retry_count: int = 3,
363
343
  ) -> None:
344
+ """
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次)
351
+ """
364
352
  if self._closed:
365
353
  raise RuntimeError("客户端已关闭,无法发布消息")
366
354
 
355
+ # 处理消息体序列化
367
356
  try:
368
357
  if isinstance(message_body, MQMsgModel):
369
358
  body = json.dumps(message_body.to_dict(),
@@ -376,72 +365,93 @@ class RabbitMQClient:
376
365
  else:
377
366
  raise TypeError(f"不支持的消息体类型: {type(message_body)}")
378
367
  except Exception as e:
379
- logger.error(f"消息体序列化失败: {e}")
368
+ logger.error(f"消息体序列化失败: {str(e)}", exc_info=True)
380
369
  raise
381
370
 
382
- message = Message(body=body, headers=headers or {},
383
- content_type=content_type, delivery_mode=delivery_mode)
384
- last_exception = None
371
+ # 构建消息对象
372
+ message = Message(
373
+ body=body,
374
+ headers=headers or {},
375
+ content_type=content_type,
376
+ delivery_mode=delivery_mode,
377
+ )
385
378
 
379
+ # 发布重试逻辑
386
380
  for retry in range(retry_count):
387
381
  try:
382
+ # 确保连接有效
388
383
  if not await self.is_connected:
384
+ logger.warning(f"发布消息前连接失效,触发重连(retry: {retry})")
389
385
  await self.connect()
390
386
 
391
- result = await self._exchange.publish(
387
+ # 核心:发布消息(mandatory=True 确保路由有效,timeout=5s 避免阻塞)
388
+ publish_result = await self._exchange.publish(
392
389
  message=message,
393
390
  routing_key=self.routing_key or self.queue_name or "#",
394
391
  mandatory=True,
395
392
  timeout=5.0
396
393
  )
397
394
 
398
- if result is None:
399
- raise RuntimeError(f"消息未找到匹配的队列: {self.routing_key}")
395
+ # 处理 mandatory 未路由场景
396
+ if publish_result is None:
397
+ raise RuntimeError(
398
+ f"消息未找到匹配的队列(routing_key: {self.routing_key}),mandatory=True 触发失败"
399
+ )
400
400
 
401
- logger.info(f"发布成功: {self.routing_key}")
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
+ )
402
405
  return
403
-
406
+ except asyncio.TimeoutError:
407
+ logger.error(
408
+ f"消息发布超时(retry: {retry}/{retry_count-1}),超时时间: 5.0s"
409
+ )
404
410
  except RuntimeError as e:
405
- if "未找到匹配的队列" in str(e):
406
- raise
407
- last_exception = str(e)
408
- await self._handle_publish_failure()
409
-
411
+ logger.error(
412
+ f"消息发布业务失败(retry: {retry}/{retry_count-1}): {str(e)}"
413
+ )
410
414
  except Exception as e:
411
- last_exception = str(e)
412
- logger.error(f"发布异常: {e}")
413
- await self._handle_publish_failure()
414
-
415
- await asyncio.sleep(5)
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))
416
423
 
417
- raise RuntimeError(f"消息发布最终失败: {last_exception}")
424
+ # 所有重试失败
425
+ raise RuntimeError(
426
+ f"消息发布失败(已重试{retry_count}次),routing_key: {self.routing_key},"
427
+ f"mandatory: True,timeout: 5.0s"
428
+ )
418
429
 
419
430
  async def close(self) -> None:
431
+ """关闭客户端(移除回调+释放资源)"""
420
432
  self._closed = True
421
433
  logger.info("开始关闭RabbitMQ客户端...")
422
434
 
435
+ # 停止重连任务
423
436
  if self._current_reconnect_task and not self._current_reconnect_task.done():
424
437
  self._current_reconnect_task.cancel()
425
438
  try:
426
439
  await self._current_reconnect_task
427
440
  except asyncio.CancelledError:
428
- pass
441
+ logger.debug("重连任务已取消")
429
442
 
443
+ # 1. 停止消费
430
444
  await self.stop_consuming()
431
445
 
446
+ # 2. 清理回调+状态(单通道无需归还,连接池统一管理)
432
447
  async with self._connect_lock:
433
448
  if self._conn_close_callback and self._channel_conn:
434
449
  self._channel_conn.close_callbacks.discard(
435
450
  self._conn_close_callback)
436
-
437
451
  self._channel = None
438
452
  self._channel_conn = None
439
453
  self._exchange = None
440
454
  self._queue = None
441
455
  self._message_handler = None
442
456
 
443
- # 确保唤醒可能正在等待 connect 的任务
444
- self._connecting = False
445
- self._connect_condition.notify_all()
446
-
447
- logger.info("客户端已关闭")
457
+ logger.info("RabbitMQ客户端已完全关闭")