sycommon-python-lib 0.2.0b3__tar.gz → 0.2.0b4__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 (96) hide show
  1. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/PKG-INFO +1 -1
  2. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/pyproject.toml +1 -1
  3. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_client.py +125 -71
  4. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_pool.py +46 -81
  5. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_client_manager.py +2 -2
  6. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +1 -1
  7. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +2 -2
  8. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_producer_manager.py +8 -8
  9. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
  10. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/README.md +0 -0
  11. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/setup.cfg +0 -0
  12. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/command/cli.py +0 -0
  13. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/__init__.py +0 -0
  14. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/Config.py +0 -0
  15. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/DatabaseConfig.py +0 -0
  16. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/EmbeddingConfig.py +0 -0
  17. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/LLMConfig.py +0 -0
  18. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/LangfuseConfig.py +0 -0
  19. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/MQConfig.py +0 -0
  20. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/RerankerConfig.py +0 -0
  21. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/SentryConfig.py +0 -0
  22. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/__init__.py +0 -0
  23. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/async_base_db_service.py +0 -0
  24. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/async_database_service.py +0 -0
  25. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/base_db_service.py +0 -0
  26. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/database_service.py +0 -0
  27. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/__init__.py +0 -0
  28. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/health_check.py +0 -0
  29. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/metrics.py +0 -0
  30. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/ping.py +0 -0
  31. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/__init__.py +0 -0
  32. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/embedding.py +0 -0
  33. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/get_llm.py +0 -0
  34. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/llm_logger.py +0 -0
  35. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/llm_tokens.py +0 -0
  36. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/struct_token.py +0 -0
  37. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/sy_langfuse.py +0 -0
  38. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/usage_token.py +0 -0
  39. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/__init__.py +0 -0
  40. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/async_sql_logger.py +0 -0
  41. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/kafka_log.py +0 -0
  42. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/logger_levels.py +0 -0
  43. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/logger_wrapper.py +0 -0
  44. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/sql_logger.py +0 -0
  45. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/__init__.py +0 -0
  46. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/context.py +0 -0
  47. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/cors.py +0 -0
  48. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/docs.py +0 -0
  49. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/exception.py +0 -0
  50. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/middleware.py +0 -0
  51. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/monitor_memory.py +0 -0
  52. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/mq.py +0 -0
  53. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/timeout.py +0 -0
  54. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/traceid.py +0 -0
  55. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/__init__.py +0 -0
  56. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/base_http.py +0 -0
  57. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/log.py +0 -0
  58. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqlistener_config.py +0 -0
  59. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqmsg_model.py +0 -0
  60. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqsend_config.py +0 -0
  61. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/sso_user.py +0 -0
  62. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/notice/__init__.py +0 -0
  63. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/notice/uvicorn_monitor.py +0 -0
  64. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
  65. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_core.py +0 -0
  66. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/sentry/__init__.py +0 -0
  67. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/sentry/sy_sentry.py +0 -0
  68. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/services.py +0 -0
  69. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/sse/__init__.py +0 -0
  70. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/sse/event.py +0 -0
  71. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/sse/sse.py +0 -0
  72. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/__init__.py +0 -0
  73. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/example.py +0 -0
  74. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/example2.py +0 -0
  75. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/feign.py +0 -0
  76. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/feign_client.py +0 -0
  77. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_client_base.py +0 -0
  78. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_config_manager.py +0 -0
  79. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_heartbeat_manager.py +0 -0
  80. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service.py +0 -0
  81. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service_discovery.py +0 -0
  82. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service_registration.py +0 -0
  83. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/param.py +0 -0
  84. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tests/test_email.py +0 -0
  85. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/__init__.py +0 -0
  86. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/docs.py +0 -0
  87. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/env.py +0 -0
  88. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/merge_headers.py +0 -0
  89. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/snowflake.py +0 -0
  90. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/syemail.py +0 -0
  91. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/timing.py +0 -0
  92. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
  93. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  94. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  95. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
  96. {sycommon_python_lib-0.2.0b3 → sycommon_python_lib-0.2.0b4}/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.2.0b3
3
+ Version: 0.2.0b4
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.2.0b3"
3
+ version = "0.2.0b4"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import functools
2
3
  import json
3
4
  from typing import Optional, Callable, Coroutine, Dict, Any, Union
4
5
  from aio_pika import Channel, Message, DeliveryMode, ExchangeType
@@ -67,6 +68,7 @@ class RabbitMQClient:
67
68
  # 并发控制
68
69
  self._consume_lock = asyncio.Lock()
69
70
  self._connect_lock = asyncio.Lock()
71
+ self._reconnect_lock = asyncio.Lock()
70
72
 
71
73
  # 防止并发重连覆盖
72
74
  self._connecting = False
@@ -78,14 +80,24 @@ class RabbitMQClient:
78
80
  self._RECONNECT_INTERVAL = 15
79
81
 
80
82
  @property
81
- async def is_connected(self) -> bool:
83
+ def is_connected(self) -> bool:
84
+ """
85
+ 同步检查连接状态
86
+ 【修复】改为同步属性,避免异步调用错误和布尔上下文误判
87
+ """
82
88
  if self._closed:
83
89
  return False
84
90
  try:
91
+ # 检查通道是否有效
92
+ if not self._channel or self._channel.is_closed:
93
+ return False
94
+
95
+ # 检查连接是否有效
96
+ if self._channel_conn and self._channel_conn.is_closed:
97
+ return False
98
+
85
99
  return (
86
- self._channel and not self._channel.is_closed
87
- and self._channel_conn and not self._channel_conn.is_closed
88
- and self._exchange is not None
100
+ self._exchange is not None
89
101
  and (not self.queue_name or self._queue is not None)
90
102
  )
91
103
  except Exception:
@@ -96,7 +108,7 @@ class RabbitMQClient:
96
108
  if not self._channel or self._channel.is_closed:
97
109
  raise RuntimeError("无有效通道,无法重建资源")
98
110
 
99
- # 无条件声明交换机
111
+ # 1. 无条件声明交换机
100
112
  exchange_inst = await self._channel.declare_exchange(
101
113
  name=self.exchange_name,
102
114
  type=self.exchange_type,
@@ -107,7 +119,7 @@ class RabbitMQClient:
107
119
  self._exchange = exchange_inst
108
120
  logger.info(f"交换机重建成功: {self.exchange_name}")
109
121
 
110
- # 仅在有队列名且符合条件时声明队列
122
+ # 2. 仅在有队列名且符合条件时声明队列
111
123
  if self.queue_name and self.queue_name.endswith(f".{self.app_name}"):
112
124
  self._queue = await self._channel.declare_queue(
113
125
  name=self.queue_name,
@@ -118,41 +130,61 @@ class RabbitMQClient:
118
130
  await self._queue.bind(exchange=exchange_inst, routing_key=self.routing_key)
119
131
  logger.info(f"队列重建成功: {self.queue_name}")
120
132
 
133
+ async def _ensure_connection_alive(self) -> AbstractRobustConnection:
134
+ """
135
+ 【新增安全辅助方法】
136
+ 动态获取当前有效的连接对象,避免引用池中已过期的连接。
137
+ 如果连接无效,会抛出异常,交由上层逻辑处理重连。
138
+ """
139
+ if not self.connection_pool or not self.connection_pool._initialized:
140
+ raise RuntimeError("连接池未初始化")
141
+
142
+ conn = self.connection_pool._connection
143
+ if not conn or conn.is_closed:
144
+ raise RuntimeError("连接池底层连接已关闭")
145
+ return conn
146
+
121
147
  async def connect(self) -> None:
122
- """连接方法(最终修正版:兼容新版 aio_pika)"""
148
+ """连接方法(终极防御版:防并发风暴、防死锁、防资源泄漏)"""
123
149
  if self._closed:
124
150
  raise RuntimeError("客户端已关闭,无法重新连接")
125
151
 
126
- # 1. 获取 Condition
127
- await self._connect_condition.acquire()
128
-
129
- try:
130
- # ===== 阶段 A: 检查状态与排队 =====
131
- if await self.is_connected:
132
- if self._connect_condition.locked():
133
- self._connect_condition.release()
134
- return
135
-
136
- if self._connecting:
137
- try:
138
- logger.debug("连接正在进行中,等待现有连接完成...")
139
- await asyncio.wait_for(self._connect_condition.wait(), timeout=60.0)
140
- except asyncio.TimeoutError:
141
- logger.warning("等待前序连接超时,强制接管...")
142
-
143
- if await self.is_connected:
152
+ # === 阶段 A: 并发控制循环 ===
153
+ while True:
154
+ await self._connect_condition.acquire()
155
+ try:
156
+ # 1. 检查是否已连接
157
+ if self.is_connected:
144
158
  if self._connect_condition.locked():
145
159
  self._connect_condition.release()
146
160
  return
147
161
 
148
- # ===== 阶段 B: 标记开始连接并释放锁 =====
149
- self._connecting = True
150
- self._connect_condition.release()
162
+ # 2. 检查是否已有协程在连接
163
+ if self._connecting:
164
+ try:
165
+ logger.debug("连接正在进行中,等待现有连接完成...")
166
+ # 等待其他协程完成
167
+ await asyncio.wait_for(self._connect_condition.wait(), timeout=60.0)
168
+ except asyncio.TimeoutError:
169
+ logger.warning("⚠️ 等待前序连接超时,重新竞争连接权...")
170
+
171
+ # 【核心修复】
172
+ # 无论是因为超时还是被唤醒,都 continue 重新循环
173
+ # 这样确保不会出现“所有协程同时醒来”的羊群效应
174
+ # 只有抢到锁的那个协程会进入连接逻辑,其他继续 wait
175
+ if self._connect_condition.locked():
176
+ self._connect_condition.release()
177
+ continue
151
178
 
152
- except Exception as e:
153
- if self._connect_condition.locked():
179
+ # 3. 抢到连接权,标记开始
180
+ self._connecting = True
154
181
  self._connect_condition.release()
155
- raise
182
+ break # <--- 跳出循环,去执行连接逻辑
183
+
184
+ except Exception as e:
185
+ if self._connect_condition.locked():
186
+ self._connect_condition.release()
187
+ raise
156
188
 
157
189
  # === 阶段 C: 执行耗时的连接逻辑 (无锁状态) ===
158
190
  connection_failed = False
@@ -164,7 +196,7 @@ class RabbitMQClient:
164
196
  # --- 步骤 1: 清理旧资源 ---
165
197
  was_consuming = self._consumer_tag is not None
166
198
 
167
- # 清理回调
199
+ # 清理旧连接的回调
168
200
  if self._channel_conn:
169
201
  try:
170
202
  if self._channel_conn.close_callbacks:
@@ -189,33 +221,38 @@ class RabbitMQClient:
189
221
  if is_consumer:
190
222
  logger.debug("获取消费者独立通道...")
191
223
  self._channel = await self.connection_pool.acquire_consumer_channel()
192
- # 【修正】回退到直接引用连接池的连接对象
193
- # 虽然在连接池切换时这个引用会变,但因为我们每次 connect 都会重新获取,
194
- # 且在 connect 开始时清理了旧回调,所以是安全的。
195
- self._channel_conn = self.connection_pool._connection
196
224
  else:
197
225
  logger.debug("获取生产者主通道...")
198
226
  self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
199
227
 
200
- # --- 步骤 3: 设置连接关闭回调 ---
228
+ # --- 步骤 3: 统一获取并注册连接回调 ---
229
+ if not self._channel_conn:
230
+ # 消费者路径需要手动获取连接引用
231
+ # 直接引用连接池的连接对象,确保引用时效性
232
+ self._channel_conn = self.connection_pool._connection
233
+
201
234
  loop = asyncio.get_running_loop()
202
235
 
203
236
  def on_conn_closed(conn, exc):
204
- # 双重检查,防止关闭过程中触发重连
205
- if self._closed or self._connecting:
206
- return
207
- logger.warning(f"检测到底层连接关闭: {exc}")
208
- asyncio.run_coroutine_threadsafe(self._safe_reconnect(), loop)
237
+ # 【核心修复】不再判断 _connecting,直接委托给 _safe_reconnect
238
+ # _safe_reconnect 内部有锁,会自动处理并发问题
239
+ if not self._closed:
240
+ logger.warning(f"⚠️ 检测到底层连接关闭: {exc}")
241
+ asyncio.run_coroutine_threadsafe(
242
+ self._safe_reconnect(), loop)
243
+
244
+ # 注册回调前,再次防御性清理
245
+ if self._channel_conn.close_callbacks:
246
+ self._channel_conn.close_callbacks.clear()
209
247
 
210
- if self._channel_conn:
211
- self._channel_conn.close_callbacks.add(on_conn_closed)
248
+ self._channel_conn.close_callbacks.add(on_conn_closed)
212
249
 
213
250
  # --- 步骤 4: 重建基础资源 ---
214
251
  await self._rebuild_resources()
215
252
 
216
253
  except Exception as e:
217
254
  connection_failed = True
218
- logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
255
+ logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
219
256
 
220
257
  # 异常时清理
221
258
  if self._channel_conn and self._channel_conn.close_callbacks:
@@ -233,7 +270,7 @@ class RabbitMQClient:
233
270
  try:
234
271
  await self._connect_condition.acquire()
235
272
  except Exception:
236
- # 如果 acquire 本身失败,确保状态复位
273
+ # 如果 acquire 本身失败,确保状态复位,防止死锁
237
274
  self._connecting = False
238
275
  self._connect_condition.notify_all()
239
276
  raise
@@ -286,29 +323,27 @@ class RabbitMQClient:
286
323
  self._connect_condition.release()
287
324
 
288
325
  async def _safe_reconnect(self):
289
- """安全重连任务(仅用于被动监听连接关闭)"""
290
- async with self._reconnect_semaphore:
291
- if self._closed:
292
- return
293
-
294
- # 如果已经在重连,直接忽略
295
- if self._connecting:
296
- return
297
-
298
- logger.info(f"将在{self._RECONNECT_INTERVAL}秒后尝试重连...")
299
- await asyncio.sleep(self._RECONNECT_INTERVAL)
300
-
301
- if self._closed or await self.is_connected:
302
- return
326
+ """
327
+ 安全重连入口
328
+ 【核心修复】使用锁防止并发风暴,确保同一时间只有一个重连任务在执行。
329
+ """
330
+ # 如果锁已经被占用,说明已经有重连任务在进行中,直接忽略,防止无限递归
331
+ if self._reconnect_lock.locked():
332
+ logger.debug("⏳ 重连任务已在执行中,忽略本次触发,避免并发风暴")
333
+ return
303
334
 
335
+ async with self._reconnect_lock:
304
336
  try:
305
- self._current_reconnect_task = asyncio.create_task(
306
- self.connect())
307
- await self._current_reconnect_task
337
+ if self._closed:
338
+ logger.info("客户端已手动关闭,取消重连")
339
+ return
340
+
341
+ logger.warning("🔄 触发底层连接重连...")
342
+ # 调用实际的 connect 方法
343
+ await self.connect()
308
344
  except Exception as e:
309
- logger.warning(f"重连失败: {str(e)}")
310
- finally:
311
- self._current_reconnect_task = None
345
+ # 即使重连失败,也不要在这里递归调用自己,而是依赖外部的定时任务或下一次网络事件
346
+ logger.error(f"❌ 安全重连执行失败: {str(e)}", exc_info=True)
312
347
 
313
348
  async def set_message_handler(self, handler: Callable[..., Coroutine]) -> None:
314
349
  if not asyncio.iscoroutinefunction(handler):
@@ -317,23 +352,42 @@ class RabbitMQClient:
317
352
  self._message_handler = handler
318
353
 
319
354
  async def _process_message_callback(self, message: AbstractIncomingMessage):
355
+ """
356
+ 消息处理回调
357
+ """
320
358
  try:
359
+ # === 阶段 1: 消息解析与上下文设置 (在主线程执行) ===
321
360
  body_dict = json.loads(message.body.decode("utf-8"))
322
361
  msg_obj: MQMsgModel = MQMsgModel(**body_dict)
362
+
323
363
  if not msg_obj.traceId:
324
364
  msg_obj.traceId = message.headers.get(
325
365
  "trace-id") if message.headers else SYLogger.get_trace_id()
326
366
 
327
367
  SYLogger.set_trace_id(msg_obj.traceId)
328
368
 
369
+ # === 阶段 2: 业务逻辑执行 ===
329
370
  if self._message_handler:
330
- await self._message_handler(msg_obj, message)
371
+ def run_job_with_context():
372
+ # 【关键修复】在子线程内重新设置 TraceId
373
+ SYLogger.set_trace_id(msg_obj.traceId)
374
+
375
+ return asyncio.run(
376
+ functools.partial(
377
+ self._message_handler, msg_obj, message)()
378
+ )
379
+
380
+ await asyncio.to_thread(run_job_with_context)
331
381
 
382
+ # === 阶段 3: 消息确认 ===
332
383
  await message.ack()
333
384
 
334
385
  except Exception as e:
335
- logger.error(f"消息处理异常: {e}", exc_info=True)
336
- await message.ack()
386
+ logger.error(f"消息处理异常: {str(e)}", exc_info=True)
387
+ try:
388
+ await message.ack()
389
+ except Exception:
390
+ pass
337
391
 
338
392
  async def start_consuming(self) -> Optional[ConsumerTag]:
339
393
  if self._closed:
@@ -343,7 +397,7 @@ class RabbitMQClient:
343
397
  if not self._message_handler:
344
398
  raise RuntimeError("未设置消息处理器")
345
399
 
346
- if not await self.is_connected:
400
+ if not self.is_connected:
347
401
  await self.connect()
348
402
 
349
403
  if not self._queue:
@@ -421,7 +475,7 @@ class RabbitMQClient:
421
475
 
422
476
  for retry in range(retry_count):
423
477
  try:
424
- if not await self.is_connected:
478
+ if not self.is_connected:
425
479
  await self.connect()
426
480
 
427
481
  result = await self._exchange.publish(
@@ -23,7 +23,7 @@ class AsyncProperty:
23
23
 
24
24
 
25
25
  class RabbitMQConnectionPool:
26
- """单连接单通道RabbitMQ客户端 (严格执行“先清理后连接”策略)"""
26
+ """单连接单通道RabbitMQ客户端"""
27
27
 
28
28
  def __init__(
29
29
  self,
@@ -73,6 +73,7 @@ class RabbitMQConnectionPool:
73
73
  return False
74
74
  if not self._initialized:
75
75
  return False
76
+ # 修复:简化检查,移除耗时的 connect() 调用,直接检查状态
76
77
  if self._connection is None or self._connection.is_closed:
77
78
  return False
78
79
  if self._channel is None or self._channel.is_closed:
@@ -112,7 +113,6 @@ class RabbitMQConnectionPool:
112
113
  if self._connection:
113
114
  try:
114
115
  if not self._connection.is_closed:
115
- # close() 可能是同步的,也可能是异步的,aio_pika 中通常是异步的
116
116
  await self._connection.close()
117
117
  except Exception as e:
118
118
  logger.warning(f"⚠️ [CLEANUP_CONN] 关闭连接失败: {e}")
@@ -121,67 +121,29 @@ class RabbitMQConnectionPool:
121
121
 
122
122
  logger.info("✅ [CLEANUP] 资源清理完成")
123
123
 
124
- async def _create_connection_impl(self, host: str) -> AbstractRobustConnection:
125
- conn_url = (
126
- f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
127
- f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
128
- f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
129
- )
130
- logger.info(f"🔌 [CONNECT] 尝试连接节点: {host}")
131
- try:
132
- conn = await asyncio.wait_for(
133
- connect_robust(conn_url),
134
- timeout=self.connection_timeout + 5
135
- )
136
- logger.info(f"✅ [CONNECT_OK] 节点连接成功: {host}")
137
- return conn
138
- except Exception as e:
139
- logger.error(f"❌ [CONNECT_FAIL] 节点 {host} 连接失败: {str(e)}")
140
- raise ConnectionError(f"无法连接RabbitMQ {host}") from e
141
-
142
124
  async def _ensure_main_channel(self) -> RobustChannel:
143
125
  """
144
- 确保主通道有效 (修复:将探活逻辑移入锁内,防止死锁)
126
+ 确保主通道有效 (修复版:移除会导致死锁的代码)
145
127
  """
146
128
  async with self._lock: # 持有锁贯穿整个方法
147
129
  if self._is_shutdown:
148
130
  raise RuntimeError("客户端已关闭")
149
131
 
150
132
  # --- 阶段 A: 连接检查与重建 ---
133
+ # 修复:直接检查 is_closed,不调用 connect(timeout=1) 防止阻塞
151
134
  connection_is_dead = False
152
- if self._connection is None:
135
+ if self._connection is None or self._connection.is_closed:
153
136
  connection_is_dead = True
154
- else:
155
- try:
156
- if self._connection.is_closed:
157
- connection_is_dead = True
158
- else:
159
- # 【修复】显式探活,保持在锁内
160
- # 这样如果探活失败,可以直接在锁内进入重建流程,无需重新竞争锁
161
- try:
162
- await asyncio.wait_for(self._connection.connect(timeout=1), timeout=15)
163
- except Exception:
164
- logger.warning("⚠️ 连接探活失败,判定为死连接,强制重建...")
165
- connection_is_dead = True
166
- except Exception:
167
- connection_is_dead = True
168
137
 
169
- # 如果连接死掉,执行清理和重建
170
138
  if connection_is_dead:
171
139
  await self._cleanup_resources()
172
- # ... (重建连接逻辑保持不变: 遍历 hosts -> connect_robust -> 赋值 self._connection) ...
173
- # 确保重建成功,否则抛出异常
174
140
 
175
- # 为了代码完整性,这里补全重建逻辑的核心部分(基于你之前的代码)
141
+ # 重建连接逻辑
176
142
  retry_hosts = self.hosts.copy()
177
143
  random.shuffle(retry_hosts)
178
144
  last_error = None
179
- max_attempts = min(len(retry_hosts), 3)
180
145
 
181
- for _ in range(max_attempts):
182
- if not retry_hosts:
183
- break
184
- host = retry_hosts.pop()
146
+ for host in retry_hosts:
185
147
  self._current_host = host
186
148
  temp_conn = None
187
149
  try:
@@ -192,8 +154,13 @@ class RabbitMQConnectionPool:
192
154
  )
193
155
  temp_conn = await asyncio.wait_for(
194
156
  connect_robust(conn_url),
195
- timeout=self.connection_timeout + 5
157
+ timeout=self.connection_timeout
196
158
  )
159
+
160
+ if self._is_shutdown:
161
+ await temp_conn.close()
162
+ raise RuntimeError("客户端已关闭")
163
+
197
164
  self._connection = temp_conn
198
165
  self._initialized = True
199
166
  last_error = None
@@ -211,10 +178,11 @@ class RabbitMQConnectionPool:
211
178
  await asyncio.sleep(self.reconnect_interval)
212
179
 
213
180
  if last_error:
214
- raise ConnectionError("所有 RabbitMQ 节点连接失败") from last_error
181
+ self._initialized = False
182
+ raise ConnectionError(
183
+ f"所有 RabbitMQ 节点连接失败: {last_error}") from last_error
215
184
 
216
185
  # --- 阶段 B: 通道恢复逻辑 ---
217
- # 此时 self._connection 必须是有效的
218
186
  if self._channel is None or self._channel.is_closed:
219
187
  max_channel_attempts = 2
220
188
  for attempt in range(max_channel_attempts):
@@ -227,18 +195,19 @@ class RabbitMQConnectionPool:
227
195
  logger.warning(
228
196
  f"⚠️ [CHANNEL_RETRY] 第 {attempt + 1} 次尝试创建通道失败: {e}")
229
197
  if attempt < max_channel_attempts - 1:
230
- await self._cleanup_resources() # 通道失败导致连接可能也坏了,重置
231
- # 简单重试逻辑:这里抛出异常让外层重试,或者在这里递归调用
232
- # 鉴于复杂度,建议抛出异常
233
- raise e
198
+ try:
199
+ await self._connection.close()
200
+ except:
201
+ pass
202
+ self._connection = None
234
203
  else:
235
- raise e
204
+ raise RuntimeError(f"创建通道失败: {e}")
236
205
  else:
237
- # 通道存在,进行一次轻量级探活 (保持一致性)
206
+ # 通道存在,进行一次轻量级 QOS 刷新
238
207
  try:
239
208
  await self._channel.set_qos(prefetch_count=self.prefetch_count)
240
209
  except Exception:
241
- logger.warning("⚠️ 通道探活失败,重建通道...")
210
+ logger.warning("⚠️ 通道 QOS 刷新失败,重建通道...")
242
211
  self._channel = await self._connection.channel()
243
212
  await self._channel.set_qos(prefetch_count=self.prefetch_count)
244
213
 
@@ -246,14 +215,12 @@ class RabbitMQConnectionPool:
246
215
 
247
216
  async def init_pools(self):
248
217
  """初始化入口"""
249
- # 快速检查
250
218
  if self._initialized:
251
219
  return
252
220
 
253
221
  conn_created_in_this_try = None
254
222
 
255
223
  try:
256
- # 锁外创建连接,减少锁持有时间
257
224
  init_host = random.choice(self.hosts)
258
225
  conn = await self._create_connection_impl(init_host)
259
226
  conn_created_in_this_try = conn
@@ -262,14 +229,12 @@ class RabbitMQConnectionPool:
262
229
  if self._is_shutdown:
263
230
  raise RuntimeError("客户端已关闭")
264
231
 
265
- # 双重检查:防止在锁外等待时,状态已改变或被其他协程初始化
266
232
  if self._initialized:
267
233
  logger.info("🚀 [INIT_SKIP] 其他协程已完成初始化")
268
234
  if conn_created_in_this_try:
269
235
  await conn_created_in_this_try.close()
270
236
  return
271
237
 
272
- # 提交新资源
273
238
  self._connection = conn
274
239
  self._channel = await self._connection.channel()
275
240
  await self._channel.set_qos(prefetch_count=self.prefetch_count)
@@ -286,29 +251,38 @@ class RabbitMQConnectionPool:
286
251
  except Exception:
287
252
  pass
288
253
  if not self._is_shutdown:
289
- await self.close()
254
+ async with self._lock:
255
+ self._initialized = False
290
256
  raise
291
257
 
258
+ async def _create_connection_impl(self, host: str) -> AbstractRobustConnection:
259
+ conn_url = (
260
+ f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
261
+ f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
262
+ f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
263
+ )
264
+ logger.info(f"🔌 [CONNECT] 尝试连接节点: {host}")
265
+ try:
266
+ conn = await asyncio.wait_for(
267
+ connect_robust(conn_url),
268
+ timeout=self.connection_timeout + 5
269
+ )
270
+ logger.info(f"✅ [CONNECT_OK] 节点连接成功: {host}")
271
+ return conn
272
+ except Exception as e:
273
+ logger.error(f"❌ [CONNECT_FAIL] 节点 {host} 连接失败: {str(e)}")
274
+ raise ConnectionError(f"无法连接RabbitMQ {host}") from e
275
+
292
276
  async def force_reconnect(self):
293
- """
294
- 强制重连
295
- 严格执行:清理所有资源 -> 尝试建立新资源
296
- """
277
+ """强制重连"""
297
278
  async with self._lock:
298
279
  if self._is_shutdown:
299
280
  return
300
281
 
301
282
  logger.warning("🔄 [FORCE_RECONNECT] 开始强制重连...")
302
-
303
- # 1. 【关键】标记未初始化,迫使 _ensure_main_channel 走清理流程
304
283
  self._initialized = False
305
-
306
- # 2. 【关键】立即清理旧资源 (在锁内)
307
284
  await self._cleanup_resources()
308
285
 
309
- # 此时 self._connection 和 self._channel 均为 None
310
-
311
- # 3. 锁外触发恢复 (避免阻塞锁太久)
312
286
  try:
313
287
  await self.acquire_channel()
314
288
  logger.info("✅ [FORCE_RECONNECT_OK] 强制重连成功")
@@ -317,20 +291,13 @@ class RabbitMQConnectionPool:
317
291
  raise
318
292
 
319
293
  async def acquire_consumer_channel(self) -> RobustChannel:
320
- """
321
- 专门为消费者获取独立的通道。
322
- 遵循 aio_pika 最佳实践:消费者不应与发布者或其他消费者共享同一个 Channel 对象。
323
- """
324
- # 确保连接池已初始化且连接是活的
294
+ """获取消费者独立通道"""
325
295
  if not self._initialized:
326
296
  await self.init_pools()
327
297
 
328
- # 确保 self._connection 是有效的(复用 _ensure_main_channel 的连接恢复逻辑)
329
298
  await self._ensure_main_channel()
330
299
 
331
- # 基于有效连接创建一个新的独立通道
332
300
  try:
333
- # 注意:这里直接使用 self._connection,而不是返回缓存的 self._channel
334
301
  consumer_ch = await self._connection.channel()
335
302
  await consumer_ch.set_qos(prefetch_count=self.prefetch_count)
336
303
  logger.debug("✅ [CONSUMER_CH] 消费者独立通道已创建")
@@ -424,11 +391,9 @@ class RabbitMQConnectionPool:
424
391
  async def close(self):
425
392
  """资源销毁"""
426
393
  try:
427
- # 设置超时,防止因锁异常导致无法关闭
428
394
  await asyncio.wait_for(self._lock.acquire(), timeout=5.0)
429
395
  except asyncio.TimeoutError:
430
396
  logger.error("⚠️ [CLOSE_TIMEOUT] 获取锁超时,强制标记关闭")
431
- # 如果拿不到锁,我们只能标记状态,无法安全清理连接
432
397
  self._is_shutdown = True
433
398
  self._initialized = False
434
399
  return
@@ -85,7 +85,7 @@ class RabbitMQClientManager(RabbitMQCoreService):
85
85
  await client.connect()
86
86
 
87
87
  # 验证重连结果
88
- if await client.is_connected:
88
+ if client.is_connected:
89
89
  logger.info(f"✅ 客户端 '{client_name}' 重连成功")
90
90
  return True
91
91
  else:
@@ -199,7 +199,7 @@ class RabbitMQClientManager(RabbitMQCoreService):
199
199
  if not client.queue_name and kwargs.get("queue_name"):
200
200
  client.queue_name = kwargs.get("queue_name")
201
201
 
202
- if await client.is_connected:
202
+ if client.is_connected:
203
203
  return client
204
204
  else:
205
205
  logger.info(f"客户端 '{client_name}' 连接已断开,重新创建")
@@ -33,7 +33,7 @@ class RabbitMQConnectionMonitor(RabbitMQClientManager):
33
33
  # 检查所有客户端连接
34
34
  for client_name, client in list(cls._clients.items()):
35
35
  try:
36
- client_connected = await client.is_connected
36
+ client_connected = client.is_connected
37
37
  if not client_connected:
38
38
  logger.warning(
39
39
  f"客户端 '{client_name}' 连接异常,触发重连")
@@ -160,7 +160,7 @@ class RabbitMQConsumerManager(RabbitMQClientManager):
160
160
 
161
161
  # 确保客户端已连接
162
162
  start_time = asyncio.get_event_loop().time()
163
- while not await client.is_connected and not cls._is_shutdown:
163
+ while not client.is_connected and not cls._is_shutdown:
164
164
  if asyncio.get_event_loop().time() - start_time > cls.CONSUMER_START_TIMEOUT:
165
165
  raise TimeoutError(f"等待客户端 '{client_name}' 连接超时")
166
166
 
@@ -184,7 +184,7 @@ class RabbitMQConsumerManager(RabbitMQClientManager):
184
184
  while attempt < max_attempts and not stop_event.is_set() and not cls._is_shutdown:
185
185
  try:
186
186
  # 启动消费前再次校验
187
- if not await client.is_connected:
187
+ if not client.is_connected:
188
188
  logger.info(f"消费者 '{client_name}' 连接断开,尝试重连")
189
189
  await client.connect()
190
190
 
@@ -56,7 +56,7 @@ class RabbitMQProducerManager(RabbitMQClientManager):
56
56
  # ===== 处理已有客户端重连 =====
57
57
  if normalized_name in cls._clients:
58
58
  client = cls._clients[normalized_name]
59
- if not await client.is_connected:
59
+ if not client.is_connected:
60
60
  client.queue_name = normalized_name
61
61
  client.create_if_not_exists = False
62
62
  await client.connect()
@@ -104,14 +104,14 @@ class RabbitMQProducerManager(RabbitMQClientManager):
104
104
  # 检查是否在已注册的发送器中
105
105
  if queue_name in cls._sender_client_names and queue_name in cls._clients:
106
106
  client = cls._clients[queue_name]
107
- if await client.is_connected:
107
+ if client.is_connected:
108
108
  return client
109
109
  else:
110
110
  logger.info(f"发送器 '{queue_name}' 连接已断开,尝试重连")
111
111
  try:
112
112
  client.create_if_not_exists = False
113
113
  await client.connect()
114
- if await client.is_connected:
114
+ if client.is_connected:
115
115
  return client
116
116
  except Exception as e:
117
117
  logger.error(f"发送器 '{queue_name}' 重连失败: {str(e)}")
@@ -123,14 +123,14 @@ class RabbitMQProducerManager(RabbitMQClientManager):
123
123
  suffixed_name = f"{queue_name}.{app_name}"
124
124
  if suffixed_name in cls._sender_client_names and suffixed_name in cls._clients:
125
125
  client = cls._clients[suffixed_name]
126
- if await client.is_connected:
126
+ if client.is_connected:
127
127
  return client
128
128
  else:
129
129
  logger.info(f"发送器 '{suffixed_name}' 连接已断开,尝试重连")
130
130
  try:
131
131
  client.create_if_not_exists = False
132
132
  await client.connect()
133
- if await client.is_connected:
133
+ if client.is_connected:
134
134
  return client
135
135
  except Exception as e:
136
136
  logger.error(f"发送器 '{suffixed_name}' 重连失败: {str(e)}")
@@ -156,7 +156,7 @@ class RabbitMQProducerManager(RabbitMQClientManager):
156
156
  raise ValueError(error_msg)
157
157
 
158
158
  # 确保连接有效
159
- if not await sender.is_connected:
159
+ if not sender.is_connected:
160
160
  logger.info(f"发送器 '{queue_name}' 连接已关闭,尝试重新连接")
161
161
  max_retry = 3
162
162
  retry_count = 0
@@ -166,7 +166,7 @@ class RabbitMQProducerManager(RabbitMQClientManager):
166
166
  try:
167
167
  sender.create_if_not_exists = False
168
168
  await sender.connect()
169
- if await sender.is_connected:
169
+ if sender.is_connected:
170
170
  logger.info(
171
171
  f"发送器 '{queue_name}' 第 {retry_count + 1} 次重连成功")
172
172
  break
@@ -177,7 +177,7 @@ class RabbitMQProducerManager(RabbitMQClientManager):
177
177
  f"发送器 '{queue_name}' 第 {retry_count} 次重连失败: {str(e)}")
178
178
  await asyncio.sleep(cls.RECONNECT_INTERVAL)
179
179
 
180
- if retry_count >= max_retry and not await sender.is_connected:
180
+ if retry_count >= max_retry and not sender.is_connected:
181
181
  error_msg = f"发送器 '{queue_name}' 经过 {max_retry} 次重连仍失败"
182
182
  logger.error(f"{error_msg}: {str(last_exception)}")
183
183
  raise Exception(error_msg) from last_exception
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.0b3
3
+ Version: 0.2.0b4
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown