sycommon-python-lib 0.1.56b4__tar.gz → 0.1.56b6__tar.gz

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 (77) hide show
  1. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/PKG-INFO +1 -1
  2. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/pyproject.toml +1 -1
  3. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/rabbitmq/rabbitmq_client.py +90 -33
  4. sycommon_python_lib-0.1.56b6/src/sycommon/rabbitmq/rabbitmq_pool.py +370 -0
  5. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/rabbitmq/rabbitmq_service.py +52 -41
  6. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
  7. sycommon_python_lib-0.1.56b4/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -338
  8. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/README.md +0 -0
  9. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/setup.cfg +0 -0
  10. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/command/cli.py +0 -0
  11. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/__init__.py +0 -0
  12. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/Config.py +0 -0
  13. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/DatabaseConfig.py +0 -0
  14. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/EmbeddingConfig.py +0 -0
  15. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/LLMConfig.py +0 -0
  16. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/MQConfig.py +0 -0
  17. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/RerankerConfig.py +0 -0
  18. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/config/__init__.py +0 -0
  19. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/database/async_base_db_service.py +0 -0
  20. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/database/async_database_service.py +0 -0
  21. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/database/base_db_service.py +0 -0
  22. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/database/database_service.py +0 -0
  23. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/health/__init__.py +0 -0
  24. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/health/health_check.py +0 -0
  25. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/health/metrics.py +0 -0
  26. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/health/ping.py +0 -0
  27. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/llm/__init__.py +0 -0
  28. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/llm/embedding.py +0 -0
  29. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/llm/get_llm.py +0 -0
  30. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/llm/llm_logger.py +0 -0
  31. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/llm/llm_tokens.py +0 -0
  32. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/logging/__init__.py +0 -0
  33. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/logging/async_sql_logger.py +0 -0
  34. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/logging/kafka_log.py +0 -0
  35. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/logging/logger_levels.py +0 -0
  36. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/logging/logger_wrapper.py +0 -0
  37. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/logging/sql_logger.py +0 -0
  38. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/__init__.py +0 -0
  39. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/context.py +0 -0
  40. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/cors.py +0 -0
  41. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/docs.py +0 -0
  42. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/exception.py +0 -0
  43. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/middleware.py +0 -0
  44. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/monitor_memory.py +0 -0
  45. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/mq.py +0 -0
  46. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/timeout.py +0 -0
  47. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/middleware/traceid.py +0 -0
  48. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/__init__.py +0 -0
  49. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/base_http.py +0 -0
  50. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/log.py +0 -0
  51. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/mqlistener_config.py +0 -0
  52. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/mqmsg_model.py +0 -0
  53. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/mqsend_config.py +0 -0
  54. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/models/sso_user.py +0 -0
  55. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/notice/__init__.py +0 -0
  56. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/notice/uvicorn_monitor.py +0 -0
  57. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/services.py +0 -0
  58. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/sse/__init__.py +0 -0
  59. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/sse/event.py +0 -0
  60. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/sse/sse.py +0 -0
  61. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/__init__.py +0 -0
  62. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/example.py +0 -0
  63. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/example2.py +0 -0
  64. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/feign.py +0 -0
  65. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/feign_client.py +0 -0
  66. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/nacos_service.py +0 -0
  67. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/synacos/param.py +0 -0
  68. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/tools/__init__.py +0 -0
  69. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/tools/docs.py +0 -0
  70. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/tools/merge_headers.py +0 -0
  71. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/tools/snowflake.py +0 -0
  72. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon/tools/timing.py +0 -0
  73. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
  74. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  75. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  76. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
  77. {sycommon_python_lib-0.1.56b4 → sycommon_python_lib-0.1.56b6}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.56b4
3
+ Version: 0.1.56b6
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.56-beta4"
3
+ version = "0.1.56-beta6"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -145,10 +145,15 @@ class RabbitMQClient:
145
145
  raise RuntimeError("客户端已关闭,无法重新连接")
146
146
 
147
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)
148
+ # 1. 清理旧连接回调(防止内存泄漏)
149
+ if self._channel_conn and self._conn_close_callback:
150
+ try:
151
+ self._channel_conn.close_callbacks.discard(
152
+ self._conn_close_callback)
153
+ except Exception:
154
+ pass
155
+
156
+ # 2. 清理状态
152
157
  self._channel = None
153
158
  self._channel_conn = None
154
159
  self._exchange = None
@@ -156,18 +161,14 @@ class RabbitMQClient:
156
161
  self._conn_close_callback = None
157
162
 
158
163
  try:
159
- # 从单通道池获取通道+连接(连接池自动确保通道有效)
164
+ # 3. 获取新通道
160
165
  self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
161
166
 
167
+ # 4. 设置新连接回调(使用 weakref)
162
168
  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服务状态")
169
+ # 注意:这里需要访问外部的 self,使用闭包或 weakref
170
+ # 简单起见,这里用闭包,但务必在 self.close 或 self.connect 时清理回调
171
+ logger.warning(f"检测到连接关闭: {exc}")
171
172
  if not self._closed:
172
173
  asyncio.create_task(self._safe_reconnect())
173
174
 
@@ -176,20 +177,26 @@ class RabbitMQClient:
176
177
  self._channel_conn.close_callbacks.add(
177
178
  self._conn_close_callback)
178
179
 
179
- # 重建交换机/队列资源
180
+ # 5. 重建资源
180
181
  await self._rebuild_resources()
181
182
 
182
- # 重连成功,重置失败计数器
183
+ # 重置计数
183
184
  self._reconnect_fail_count = 0
184
185
  logger.info("客户端连接初始化完成")
185
186
  except Exception as e:
186
187
  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)
188
+ # 失败时也要清理可能产生的残留引用
189
+ if self._channel_conn and self._conn_close_callback:
190
+ try:
191
+ self._channel_conn.close_callbacks.discard(
192
+ self._conn_close_callback)
193
+ except Exception:
194
+ pass
195
+ # 清空状态
191
196
  self._channel = None
192
197
  self._channel_conn = None
198
+ self._conn_close_callback = None
199
+
193
200
  # 触发重连
194
201
  if not self._closed:
195
202
  asyncio.create_task(self._safe_reconnect())
@@ -238,7 +245,7 @@ class RabbitMQClient:
238
245
  logger.info("消息处理器设置成功")
239
246
 
240
247
  async def start_consuming(self) -> Optional[ConsumerTag]:
241
- """启动消息消费(支持自动重连)"""
248
+ """启动消息消费(支持自动重连 + Header 重试计数限制)"""
242
249
  if self._closed:
243
250
  raise RuntimeError("客户端已关闭,无法启动消费")
244
251
 
@@ -251,10 +258,11 @@ class RabbitMQClient:
251
258
  if not self._queue:
252
259
  raise RuntimeError("未配置队列名或队列未创建,无法启动消费")
253
260
 
254
- # 2. 定义消费回调(包含异常处理和重连逻辑)
261
+ # 2. 定义消费回调
255
262
  async def consume_callback(message: AbstractIncomingMessage):
256
263
  try:
257
264
  # 解析消息体
265
+ msg_obj: MQMsgModel
258
266
  if self.auto_parse_json:
259
267
  try:
260
268
  body_dict = json.loads(
@@ -263,19 +271,26 @@ class RabbitMQClient:
263
271
  except json.JSONDecodeError as e:
264
272
  logger.error(
265
273
  f"JSON消息解析失败: {str(e)},消息体: {message.body[:100]}...")
266
- await message.nack(requeue=False) # 解析失败,不重入队
274
+ # 解析失败通常无法重试,直接丢弃
275
+ await message.nack(requeue=False)
267
276
  return
268
277
  else:
269
278
  msg_obj = MQMsgModel(
270
279
  body=message.body.decode("utf-8"),
271
280
  routing_key=message.routing_key,
272
281
  delivery_tag=message.delivery_tag,
282
+ traceId=message.headers.get("trace-id", None),
283
+ headers=message.headers
273
284
  )
274
285
 
286
+ # 统一追踪ID
287
+ SYLogger.set_trace_id(
288
+ message.headers.get("trace-id", None))
289
+
275
290
  # 调用消息处理器
276
291
  await self._message_handler(msg_obj, message)
277
292
 
278
- # 手动ACK
293
+ # 处理成功,手动ACK
279
294
  await message.ack()
280
295
  logger.debug(
281
296
  f"消息处理成功,delivery_tag: {message.delivery_tag}")
@@ -285,19 +300,61 @@ class RabbitMQClient:
285
300
  f"消息处理失败,delivery_tag: {message.delivery_tag}",
286
301
  exc_info=True
287
302
  )
288
- # 处理失败逻辑:首次失败重入队,再次失败丢弃
289
- if message.redelivered:
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:
290
313
  logger.warning(
291
- f"消息已重入队过,本次拒绝入队: {message.delivery_tag}")
314
+ f"消息重试次数已达上限({MAX_RETRY}),丢弃消息。"
315
+ f"delivery_tag: {message.delivery_tag}, routing_key: {message.routing_key}"
316
+ )
317
+ # 丢弃消息(不重新入队)
292
318
  await message.reject(requeue=False)
293
- else:
294
- logger.warning(f"消息重入队: {message.delivery_tag}")
295
- await message.nack(requeue=True)
319
+ return
296
320
 
297
- # 连接失效则触发重连
298
- if not await self.is_connected:
299
- logger.warning("连接已失效,触发客户端重连")
300
- asyncio.create_task(self._safe_reconnect())
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}"
332
+ )
333
+
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)
301
358
 
302
359
  # 3. 启动消费(单通道消费,避免阻塞发布需确保业务回调非阻塞)
303
360
  self._consumer_tag = await self._queue.consume(consume_callback)
@@ -0,0 +1,370 @@
1
+ import asyncio
2
+ import random
3
+ from typing import Optional, List, Dict, Callable, Tuple
4
+ from aio_pika import connect_robust, RobustChannel, Message
5
+ from aio_pika.abc import (
6
+ AbstractRobustConnection, AbstractQueue, AbstractExchange, AbstractMessage
7
+ )
8
+ from sycommon.logging.kafka_log import SYLogger
9
+
10
+ logger = SYLogger
11
+
12
+
13
+ class AsyncProperty:
14
+ """实现 await obj.attr 的支持"""
15
+
16
+ def __init__(self, method):
17
+ self.method = method
18
+
19
+ def __get__(self, obj, objtype=None):
20
+ if obj is None:
21
+ return self
22
+ # 关键:当访问 obj.attr 时,直接返回协程对象,而不是方法本身
23
+ return self.method(obj)
24
+
25
+
26
+ class RabbitMQConnectionPool:
27
+ """单连接单通道RabbitMQ客户端 (增强版日志)"""
28
+
29
+ def __init__(
30
+ self,
31
+ hosts: List[str],
32
+ port: int,
33
+ username: str,
34
+ password: str,
35
+ virtualhost: str = "/",
36
+ heartbeat: int = 30,
37
+ app_name: str = "",
38
+ connection_timeout: int = 30,
39
+ reconnect_interval: int = 5,
40
+ prefetch_count: int = 2,
41
+ ):
42
+ self.hosts = [host.strip() for host in hosts if host.strip()]
43
+ if not self.hosts:
44
+ raise ValueError("至少需要提供一个RabbitMQ主机地址")
45
+
46
+ self.port = port
47
+ self.username = username
48
+ self.password = password
49
+ self.virtualhost = virtualhost
50
+ self.app_name = app_name or "rabbitmq-client"
51
+ self.heartbeat = heartbeat
52
+ self.connection_timeout = connection_timeout
53
+ self.reconnect_interval = reconnect_interval
54
+ self.prefetch_count = prefetch_count
55
+
56
+ self._current_host: str = random.choice(self.hosts)
57
+ logger.info(f"[INIT] 随机选择RabbitMQ主机: {self._current_host}")
58
+
59
+ # 核心资源
60
+ self._connection: Optional[AbstractRobustConnection] = None
61
+ self._channel: Optional[RobustChannel] = None
62
+ self._consumer_channels: Dict[str, RobustChannel] = {}
63
+
64
+ # 状态控制
65
+ self._lock = asyncio.Lock()
66
+ self._initialized = False
67
+ self._is_shutdown = False
68
+
69
+ @AsyncProperty
70
+ async def is_alive(self) -> bool:
71
+ """对外暴露的连接存活状态(原子化判断)"""
72
+ async with self._lock:
73
+ if self._is_shutdown:
74
+ return False
75
+
76
+ if not self._initialized:
77
+ return False
78
+
79
+ if self._connection is None or self._connection.is_closed:
80
+ return False
81
+
82
+ # 可选:检查主通道是否存活
83
+ if self._channel is None or self._channel.is_closed:
84
+ # 如果你认为通道断了连接也算死,就保留这行;否则删除
85
+ return False
86
+
87
+ return True
88
+
89
+ async def _create_connection_impl(self) -> AbstractRobustConnection:
90
+ """
91
+ 连接创建入口
92
+ """
93
+ conn_url = (
94
+ f"amqp://{self.username}:{self.password}@{self._current_host}:{self.port}/"
95
+ f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
96
+ f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
97
+ )
98
+ logger.info(
99
+ f"🔌 [CONNECT_START] 尝试创建连接 -> {self._current_host}:{self.port}")
100
+ try:
101
+ conn = await connect_robust(conn_url, timeout=self.connection_timeout)
102
+ # 注意:connect_robust 返回时,底层 TCP 可能还在握手,但对象已创建
103
+ logger.info(f"✅ [CONNECT_OK] 连接对象创建成功: {id(conn)}")
104
+ return conn
105
+ except Exception as e:
106
+ logger.error(f"❌ [CONNECT_FAIL] 连接创建失败: {str(e)}")
107
+ raise ConnectionError(f"无法连接RabbitMQ {self._current_host}") from e
108
+
109
+ async def _create_channel_impl(self, connection: AbstractRobustConnection) -> RobustChannel:
110
+ """创建通道"""
111
+ try:
112
+ channel = await connection.channel()
113
+ await channel.set_qos(prefetch_count=self.prefetch_count)
114
+ logger.debug(f"✅ [CHANNEL_OK] 通道创建成功: {id(channel)}")
115
+ return channel
116
+ except Exception as e:
117
+ logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败: {str(e)}")
118
+ raise
119
+
120
+ async def _ensure_main_channel(self) -> RobustChannel:
121
+ """确保主通道有效 (原子操作 + 自动轮询重试 + 失败清理)"""
122
+ async with self._lock:
123
+ if self._is_shutdown:
124
+ raise RuntimeError("客户端已关闭")
125
+
126
+ # 如果连接对象不存在或已关闭,进入重连流程
127
+ if self._connection is None or self._connection.is_closed:
128
+ retry_hosts = self.hosts.copy() # 复制一份列表用于重试
129
+ last_error = None
130
+ temp_conn = None # 临时变量,用于引用本次尝试创建的连接
131
+
132
+ # 轮询尝试列表中的每一个 Host
133
+ while retry_hosts:
134
+ host = random.choice(retry_hosts)
135
+ retry_hosts.remove(host) # 从待尝试列表中移除
136
+
137
+ logger.info(f"⚠️ [RECONNECT] 尝试连接节点: {host}")
138
+ self._current_host = host
139
+ temp_conn = None # 重置临时引用
140
+
141
+ try:
142
+ conn_url = (
143
+ f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
144
+ f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
145
+ f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
146
+ )
147
+
148
+ # 尝试连接
149
+ temp_conn = await connect_robust(conn_url, timeout=self.connection_timeout)
150
+
151
+ # --- 关键点:连接成功 ---
152
+ self._connection = temp_conn
153
+ temp_conn = None # 清空临时引用,因为所有权已经移交给 self._connection
154
+
155
+ logger.info(f"🔗 [RECONNECT_OK] 成功连接到节点: {host}")
156
+ last_error = None
157
+ break # 成功,跳出循环
158
+
159
+ except Exception as e:
160
+ logger.warning(
161
+ f"❌ [RECONNECT_FAIL] 节点 {host} 不可用: {str(e)}")
162
+
163
+ # 【核心修复】清理失败的连接对象
164
+ if temp_conn is not None:
165
+ try:
166
+ logger.debug(
167
+ f"🧹 [CLEANUP] 正在关闭失败的连接对象: {id(temp_conn)}")
168
+ # 即使连接对象处于异常状态,close() 通常也是安全的
169
+ await temp_conn.close()
170
+ logger.debug(f"✅ [CLEANUP] 失败连接已关闭")
171
+ except Exception as close_err:
172
+ logger.warning(
173
+ f"⚠️ [CLEANUP_ERR] 关闭失败连接时出错: {str(close_err)}")
174
+
175
+ last_error = e
176
+ asyncio.sleep(self.reconnect_interval)
177
+ continue # 继续试下一个
178
+
179
+ # 如果所有节点都试完了还是失败
180
+ if last_error:
181
+ logger.error("💥 [RECONNECT_FATAL] 所有 RabbitMQ 节点均不可用")
182
+ raise ConnectionError("所有 RabbitMQ 节点连接失败") from last_error
183
+
184
+ # 2. 确保主通道存在
185
+ if self._channel is None or self._channel.is_closed:
186
+ logger.info("⚠️ [RECOVER_CHANNEL] 检测到主通道不存在或已关闭,开始恢复...")
187
+ self._channel = await self._create_channel_impl(self._connection)
188
+
189
+ return self._channel
190
+
191
+ async def init_pools(self):
192
+ """
193
+ 初始化入口与异常处理 (修复泄漏的关键)
194
+ """
195
+ async with self._lock:
196
+ if self._is_shutdown:
197
+ raise RuntimeError("客户端已关闭")
198
+ if self._initialized:
199
+ return
200
+
201
+ conn_created_in_this_try = None
202
+ try:
203
+ # 步骤 A: 创建连接 (在锁外进行,避免阻塞其他操作)
204
+ conn = await self._create_connection_impl()
205
+ conn_created_in_this_try = conn # 记录本次创建的对象,用于失败回滚
206
+
207
+ # 步骤 B: 更新状态和初始化通道 (在锁内进行,保证原子性)
208
+ async with self._lock:
209
+ if self._is_shutdown:
210
+ # 如果在创建连接期间,外部调用了 close,则必须立即清理刚创建的连接
211
+ logger.warning("⚠️ [ABORT] 检测到关闭信号,放弃初始化并清理资源")
212
+ raise RuntimeError("客户端已关闭")
213
+
214
+ self._connection = conn
215
+ self._channel = await self._create_channel_impl(conn)
216
+ self._initialized = True
217
+ logger.info(
218
+ f"🚀 [INIT_SUCCESS] 客户端初始化完成. ConnID: {id(self._connection)}")
219
+
220
+ except Exception as e:
221
+ logger.error(f"💥 [INIT_ERROR] 初始化流程异常: {str(e)}", exc_info=True)
222
+ # 如果步骤A成功但步骤B失败(例如通道创建失败),或者步骤B中出错,
223
+ # 必须显式关闭在步骤A中创建的连接,否则它会变成“游离连接”。
224
+ if conn_created_in_this_try:
225
+ logger.warning(
226
+ f"🧹 [LEAK_PREVENTION] 检测到初始化失败,正在显式关闭刚创建的连接: {id(conn_created_in_this_try)}")
227
+ try:
228
+ await conn_created_in_this_try.close()
229
+ logger.info(
230
+ f"✅ [CLOSE_OK] 泄漏连接已关闭: {id(conn_created_in_this_try)}")
231
+ except Exception as close_err:
232
+ logger.error(f"❌ [CLOSE_ERR] 关闭泄漏连接时出错: {str(close_err)}")
233
+
234
+ # 如果是因为中途关闭导致的错误,不需要再次调用全局 close,否则调用
235
+ if not self._is_shutdown:
236
+ await self.close()
237
+ raise
238
+
239
+ async def acquire_channel(self) -> Tuple[RobustChannel, AbstractRobustConnection]:
240
+ """获取主通道"""
241
+ if not self._initialized and not self._is_shutdown:
242
+ await self.init_pools()
243
+ return await self._ensure_main_channel(), self._connection
244
+
245
+ async def publish_message(self, routing_key: str, message_body: bytes, exchange_name: str = "", **kwargs):
246
+ """发布消息"""
247
+ channel, _ = await self.acquire_channel()
248
+ try:
249
+ exchange = channel.default_exchange if not exchange_name else await channel.get_exchange(exchange_name)
250
+ message = Message(body=message_body, **kwargs)
251
+ await exchange.publish(message, routing_key=routing_key)
252
+ logger.debug(f"📤 [PUBLISH] 消息发布成功 - RK: {routing_key}")
253
+ except Exception as e:
254
+ logger.error(f"❌ [PUBLISH_FAIL] 发布失败: {str(e)}")
255
+ raise
256
+
257
+ async def consume_queue(self, queue_name: str, callback: Callable[[AbstractMessage], asyncio.Future], auto_ack: bool = False, **kwargs):
258
+ """消费队列"""
259
+ if not self._initialized:
260
+ await self.init_pools()
261
+
262
+ async with self._lock:
263
+ if self._is_shutdown:
264
+ raise RuntimeError("客户端已关闭")
265
+ if queue_name in self._consumer_channels:
266
+ logger.warning(f"⚠️ [CONSUMER_EXISTS] 队列 {queue_name} 已在消费中")
267
+ return
268
+ if not self._connection or self._connection.is_closed:
269
+ raise RuntimeError("连接不可用,无法启动消费")
270
+
271
+ await self.declare_queue(queue_name, **kwargs)
272
+
273
+ try:
274
+ # 获取原始连接对象创建新通道
275
+ conn = self._connection
276
+ consumer_channel = await conn.channel()
277
+ await consumer_channel.set_qos(prefetch_count=self.prefetch_count)
278
+ logger.info(
279
+ f"✅ [CONSUMER_CHANNEL_OK] 消费者通道创建: {id(consumer_channel)}")
280
+
281
+ async with self._lock:
282
+ if self._is_shutdown:
283
+ await consumer_channel.close()
284
+ return
285
+ self._consumer_channels[queue_name] = consumer_channel
286
+
287
+ async def consume_callback_wrapper(message: AbstractMessage):
288
+ try:
289
+ await callback(message)
290
+ if not auto_ack:
291
+ await message.ack()
292
+ except Exception as e:
293
+ logger.error(
294
+ f"❌ [CALLBACK_ERR] 消费回调异常 {queue_name}: {str(e)}")
295
+ if not auto_ack:
296
+ await message.nack(requeue=True)
297
+
298
+ await consumer_channel.basic_consume(
299
+ queue_name, consumer_callback=consume_callback_wrapper, auto_ack=auto_ack, **kwargs
300
+ )
301
+ logger.info(f"🎧 [CONSUME_START] 开始消费队列: {queue_name}")
302
+
303
+ except Exception as e:
304
+ logger.error(f"💥 [CONSUME_ERR] 启动消费失败 {queue_name}: {str(e)}")
305
+ async with self._lock:
306
+ if queue_name in self._consumer_channels:
307
+ del self._consumer_channels[queue_name]
308
+ raise
309
+
310
+ async def close(self):
311
+ """
312
+ 资源销毁入口
313
+ """
314
+ async with self._lock:
315
+ if self._is_shutdown:
316
+ return
317
+ self._is_shutdown = True
318
+ self._initialized = False
319
+ # 记录即将关闭的连接ID
320
+ conn_to_close_id = id(
321
+ self._connection) if self._connection else None
322
+
323
+ logger.info(f"🛑 [CLOSE_START] 开始关闭客户端... (准备关闭连接: {conn_to_close_id})")
324
+
325
+ # 1. 关闭消费者通道
326
+ channels_to_close = []
327
+ async with self._lock:
328
+ channels_to_close = list(self._consumer_channels.values())
329
+ self._consumer_channels.clear()
330
+
331
+ for ch in channels_to_close:
332
+ try:
333
+ if not ch.is_closed:
334
+ await ch.close()
335
+ logger.debug(f"✅ [CLOSE_CHANNEL] 消费者通道已关闭")
336
+ except Exception as e:
337
+ logger.warning(f"❌ [CLOSE_CHANNEL_ERR] 关闭消费者通道失败: {str(e)}")
338
+
339
+ # 2. 关闭主通道
340
+ if self._channel:
341
+ try:
342
+ if not self._channel.is_closed:
343
+ await self._channel.close()
344
+ logger.info(f"✅ [CLOSE_CHANNEL] 主通道已关闭")
345
+ except Exception:
346
+ pass
347
+ self._channel = None
348
+
349
+ # 3. 关闭连接
350
+ # 确保在 finally 或显式 close 中调用 connection.close()
351
+ if self._connection:
352
+ try:
353
+ # 打印关闭操作
354
+ logger.info(f"🔌 [CLOSE_CONN] 正在关闭连接: {id(self._connection)}")
355
+ await self._connection.close()
356
+ logger.info(f"✅ [CLOSE_OK] 连接已成功关闭: {id(self._connection)}")
357
+ except Exception as e:
358
+ logger.warning(f"❌ [CLOSE_ERR] 关闭连接失败: {str(e)}")
359
+ self._connection = None
360
+
361
+ logger.info("🏁 [CLOSE_DONE] RabbitMQ客户端已完全关闭")
362
+
363
+ # --- 辅助方法省略 (declare_queue 等) ---
364
+ async def declare_queue(self, queue_name: str, **kwargs) -> AbstractQueue:
365
+ channel, _ = await self.acquire_channel()
366
+ return await channel.declare_queue(queue_name, **kwargs)
367
+
368
+ async def declare_exchange(self, exchange_name: str, exchange_type: str = "direct", **kwargs) -> AbstractExchange:
369
+ channel, _ = await self.acquire_channel()
370
+ return await channel.declare_exchange(exchange_name, exchange_type, **kwargs)