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