sycommon-python-lib 0.1.42__tar.gz → 0.1.44__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 (65) hide show
  1. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/PKG-INFO +7 -7
  2. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/pyproject.toml +11 -7
  3. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/traceid.py +1 -1
  4. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/rabbitmq/rabbitmq_client.py +83 -13
  5. sycommon_python_lib-0.1.44/src/sycommon/rabbitmq/rabbitmq_pool.py +404 -0
  6. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/rabbitmq/rabbitmq_service.py +23 -23
  7. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/services.py +2 -1
  8. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/feign.py +9 -8
  9. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/nacos_service.py +65 -46
  10. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon_python_lib.egg-info/PKG-INFO +7 -7
  11. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon_python_lib.egg-info/requires.txt +6 -6
  12. sycommon_python_lib-0.1.42/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -330
  13. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/README.md +0 -0
  14. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/setup.cfg +0 -0
  15. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/command/cli.py +0 -0
  16. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/__init__.py +0 -0
  17. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/Config.py +0 -0
  18. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/DatabaseConfig.py +0 -0
  19. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/EmbeddingConfig.py +0 -0
  20. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/LLMConfig.py +0 -0
  21. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/MQConfig.py +0 -0
  22. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/RerankerConfig.py +0 -0
  23. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/config/__init__.py +0 -0
  24. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/database/base_db_service.py +0 -0
  25. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/database/database_service.py +0 -0
  26. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/health/__init__.py +0 -0
  27. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/health/health_check.py +0 -0
  28. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/health/metrics.py +0 -0
  29. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/health/ping.py +0 -0
  30. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/logging/__init__.py +0 -0
  31. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/logging/kafka_log.py +0 -0
  32. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/logging/logger_wrapper.py +0 -0
  33. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/logging/sql_logger.py +0 -0
  34. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/__init__.py +0 -0
  35. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/context.py +0 -0
  36. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/cors.py +0 -0
  37. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/docs.py +0 -0
  38. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/exception.py +0 -0
  39. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/middleware.py +0 -0
  40. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/monitor_memory.py +0 -0
  41. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/mq.py +0 -0
  42. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/middleware/timeout.py +0 -0
  43. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/__init__.py +0 -0
  44. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/base_http.py +0 -0
  45. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/log.py +0 -0
  46. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/mqlistener_config.py +0 -0
  47. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/mqmsg_model.py +0 -0
  48. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/mqsend_config.py +0 -0
  49. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/models/sso_user.py +0 -0
  50. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/sse/__init__.py +0 -0
  51. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/sse/event.py +0 -0
  52. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/sse/sse.py +0 -0
  53. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/__init__.py +0 -0
  54. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/example.py +0 -0
  55. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/example2.py +0 -0
  56. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/feign_client.py +0 -0
  57. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/synacos/param.py +0 -0
  58. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/tools/__init__.py +0 -0
  59. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/tools/docs.py +0 -0
  60. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/tools/snowflake.py +0 -0
  61. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon/tools/timing.py +0 -0
  62. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
  63. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  64. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  65. {sycommon_python_lib-0.1.42 → sycommon_python_lib-0.1.44}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,22 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.42
3
+ Version: 0.1.44
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: aio-pika>=9.5.7
8
- Requires-Dist: aiohttp>=3.13.1
7
+ Requires-Dist: aio-pika>=9.5.8
8
+ Requires-Dist: aiohttp>=3.13.2
9
9
  Requires-Dist: decorator>=5.2.1
10
- Requires-Dist: fastapi>=0.120.0
11
- Requires-Dist: kafka-python>=2.2.15
10
+ Requires-Dist: fastapi>=0.121.2
11
+ Requires-Dist: kafka-python>=2.2.16
12
12
  Requires-Dist: loguru>=0.7.3
13
13
  Requires-Dist: mysql-connector-python>=9.5.0
14
14
  Requires-Dist: nacos-sdk-python>=2.0.9
15
- Requires-Dist: pydantic>=2.12.3
15
+ Requires-Dist: pydantic>=2.12.4
16
16
  Requires-Dist: python-dotenv>=1.2.1
17
17
  Requires-Dist: pyyaml>=6.0.3
18
18
  Requires-Dist: sqlalchemy>=2.0.44
19
- Requires-Dist: starlette>=0.48.0
19
+ Requires-Dist: starlette>=0.49.3
20
20
  Requires-Dist: uuid>=1.30
21
21
  Requires-Dist: uvicorn>=0.38.0
22
22
 
@@ -1,23 +1,23 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.42"
3
+ version = "0.1.44"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
- "aio-pika>=9.5.7",
9
- "aiohttp>=3.13.1",
8
+ "aio-pika>=9.5.8",
9
+ "aiohttp>=3.13.2",
10
10
  "decorator>=5.2.1",
11
- "fastapi>=0.120.0",
12
- "kafka-python>=2.2.15",
11
+ "fastapi>=0.121.2",
12
+ "kafka-python>=2.2.16",
13
13
  "loguru>=0.7.3",
14
14
  "mysql-connector-python>=9.5.0",
15
15
  "nacos-sdk-python>=2.0.9",
16
- "pydantic>=2.12.3",
16
+ "pydantic>=2.12.4",
17
17
  "python-dotenv>=1.2.1",
18
18
  "pyyaml>=6.0.3",
19
19
  "sqlalchemy>=2.0.44",
20
- "starlette>=0.48.0",
20
+ "starlette>=0.49.3",
21
21
  "uuid>=1.30",
22
22
  "uvicorn>=0.38.0",
23
23
  ]
@@ -25,5 +25,9 @@ dependencies = [
25
25
  [tool.setuptools]
26
26
  packages = {find = {where = ["src"]}}
27
27
 
28
+ [build-system]
29
+ requires = ["setuptools"]
30
+ build-backend = "setuptools.build_meta"
31
+
28
32
  [project.scripts]
29
33
  sycommon = "command.cli:main"
@@ -79,7 +79,7 @@ def setup_trace_id_handler(app):
79
79
 
80
80
  content_type = response.headers.get("Content-Type", "")
81
81
 
82
- # 处理 SSE 响应 - 关键修复点
82
+ # 处理 SSE 响应
83
83
  if "text/event-stream" in content_type:
84
84
  # 流式响应不能有Content-Length,移除它
85
85
  if "Content-Length" in response.headers:
@@ -85,6 +85,22 @@ class RabbitMQClient:
85
85
  # 线程安全锁
86
86
  self._consume_lock = asyncio.Lock()
87
87
  self._connect_lock = asyncio.Lock()
88
+ # 跟踪连接关闭回调(用于后续移除)
89
+ self._conn_close_callback: Optional[Callable] = None
90
+ # 控制重连频率的信号量(避免短时间内大量重连任务)
91
+ self._reconnect_semaphore = asyncio.Semaphore(1)
92
+ # 重连冷却时间(秒)
93
+ self._reconnect_cooldown = 3
94
+ # 固定重连间隔15秒(全局统一)
95
+ self._RECONNECT_INTERVAL = 15
96
+ # 重连任务锁(确保同一时间只有一个重连任务)
97
+ self._reconnect_task_lock = asyncio.Lock()
98
+ # 跟踪当前重连任务(避免重复创建)
99
+ self._current_reconnect_task: Optional[asyncio.Task] = None
100
+ # 连接失败计数器(用于告警)
101
+ self._reconnect_fail_count = 0
102
+ # 连接失败告警阈值
103
+ self._reconnect_alert_threshold = 5
88
104
 
89
105
  @property
90
106
  async def is_connected(self) -> bool:
@@ -104,14 +120,16 @@ class RabbitMQClient:
104
120
  return False
105
121
 
106
122
  async def connect(self) -> None:
107
- """建立连接并初始化交换机/队列(支持重连)"""
108
123
  if self._closed:
109
124
  raise RuntimeError("客户端已关闭,无法重新连接")
110
125
 
111
126
  async with self._connect_lock:
112
- # 释放旧的无效资源
127
+ # 释放旧资源(保留原有回调清理逻辑)
113
128
  if self._channel and self._channel_conn:
114
129
  try:
130
+ if self._conn_close_callback and self._channel_conn:
131
+ self._channel_conn.close_callbacks.discard(
132
+ self._conn_close_callback)
115
133
  await self.connection_pool.release_channel(self._channel, self._channel_conn)
116
134
  except Exception as e:
117
135
  SYLogger.warning(f"释放旧通道失败: {str(e)}")
@@ -119,21 +137,28 @@ class RabbitMQClient:
119
137
  self._channel_conn = None
120
138
  self._exchange = None
121
139
  self._queue = None
140
+ self._conn_close_callback = None
122
141
 
123
142
  try:
124
- # 1. 从连接池获取通道+连接
143
+ # 从连接池获取通道+连接(连接池已控制连接数)
125
144
  self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
126
145
 
127
146
  def on_conn_closed(conn: AbstractRobustConnection, exc: Optional[BaseException]):
128
- """连接关闭时触发的回调"""
129
- SYLogger.error(
147
+ """连接关闭回调:触发固定间隔重连"""
148
+ SYLogger.warning(
130
149
  f"客户端连接关闭: {conn!r},原因: {exc}", exc_info=exc)
150
+ self._reconnect_fail_count += 1
151
+ # 超过阈值告警
152
+ if self._reconnect_fail_count >= self._reconnect_alert_threshold:
153
+ SYLogger.error(
154
+ f"连接失败次数已达阈值({self._reconnect_alert_threshold}),请检查MQ服务状态")
131
155
  if not self._closed:
132
- asyncio.create_task(self.connect())
156
+ asyncio.create_task(self._safe_reconnect())
133
157
 
134
- # 给连接添加关闭回调
158
+ self._conn_close_callback = on_conn_closed
135
159
  if self._channel_conn:
136
- self._channel_conn.close_callbacks.add(on_conn_closed)
160
+ self._channel_conn.close_callbacks.add(
161
+ self._conn_close_callback)
137
162
 
138
163
  # 2. 设置预取计数(限流)
139
164
  await self._channel.set_qos(prefetch_count=self.prefetch_count)
@@ -168,10 +193,15 @@ class RabbitMQClient:
168
193
  f"(绑定交换机: {self.exchange_name}, routing_key: {self.routing_key})"
169
194
  )
170
195
 
196
+ # 重连成功,重置失败计数器
197
+ self._reconnect_fail_count = 0
171
198
  SYLogger.info("客户端连接初始化完成")
172
199
  except Exception as e:
173
200
  SYLogger.error(f"客户端连接失败: {str(e)}", exc_info=True)
174
201
  # 清理异常状态
202
+ if self._conn_close_callback and self._channel_conn:
203
+ self._channel_conn.close_callbacks.discard(
204
+ self._conn_close_callback)
175
205
  if self._channel and self._channel_conn:
176
206
  try:
177
207
  await self.connection_pool.release_channel(self._channel, self._channel_conn)
@@ -179,8 +209,40 @@ class RabbitMQClient:
179
209
  pass
180
210
  self._channel = None
181
211
  self._channel_conn = None
212
+ # 触发重连(固定间隔)
213
+ if not self._closed:
214
+ asyncio.create_task(self._safe_reconnect())
182
215
  raise
183
216
 
217
+ async def _safe_reconnect(self):
218
+ """安全重连:固定15秒间隔,避免重复任务"""
219
+ # 检查是否已有重连任务在运行
220
+ if self._current_reconnect_task and not self._current_reconnect_task.done():
221
+ SYLogger.debug("已有重连任务在运行,跳过重复触发")
222
+ return
223
+
224
+ async with self._reconnect_task_lock:
225
+ if self._closed or await self.is_connected:
226
+ return
227
+
228
+ # 固定15秒重连间隔
229
+ SYLogger.info(f"将在15秒后尝试重连...")
230
+ await asyncio.sleep(self._RECONNECT_INTERVAL)
231
+
232
+ if self._closed or await self.is_connected:
233
+ return
234
+
235
+ try:
236
+ self._current_reconnect_task = asyncio.create_task(
237
+ self.connect())
238
+ await self._current_reconnect_task
239
+ except Exception as e:
240
+ SYLogger.warning(f"重连失败: {str(e)}")
241
+ # 重连失败后,继续触发下一次重连(仍保持15秒间隔)
242
+ asyncio.create_task(self._safe_reconnect())
243
+ finally:
244
+ self._current_reconnect_task = None
245
+
184
246
  async def set_message_handler(
185
247
  self,
186
248
  handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]],
@@ -351,14 +413,18 @@ class RabbitMQClient:
351
413
  )
352
414
 
353
415
  async def close(self) -> None:
354
- """关闭客户端(释放资源)"""
355
- if self._closed:
356
- SYLogger.warning("客户端已关闭,无需重复操作")
357
- return
358
-
416
+ """关闭客户端(移除回调)"""
359
417
  self._closed = True
360
418
  SYLogger.info("开始关闭RabbitMQ客户端...")
361
419
 
420
+ # 停止重连任务
421
+ if self._current_reconnect_task and not self._current_reconnect_task.done():
422
+ self._current_reconnect_task.cancel()
423
+ try:
424
+ await self._current_reconnect_task
425
+ except asyncio.CancelledError:
426
+ SYLogger.debug("重连任务已取消")
427
+
362
428
  # 1. 停止消费
363
429
  await self.stop_consuming()
364
430
 
@@ -366,6 +432,10 @@ class RabbitMQClient:
366
432
  async with self._connect_lock:
367
433
  if self._channel and self._channel_conn:
368
434
  try:
435
+ # 移除连接关闭回调
436
+ if self._conn_close_callback:
437
+ self._channel_conn.close_callbacks.discard(
438
+ self._conn_close_callback)
369
439
  await self.connection_pool.release_channel(self._channel, self._channel_conn)
370
440
  SYLogger.info("通道释放成功")
371
441
  except Exception as e:
@@ -0,0 +1,404 @@
1
+ import asyncio
2
+ from typing import Optional, List, Set, Iterator, Tuple
3
+ from aio_pika import connect_robust, Channel, Message
4
+ from aio_pika.abc import (
5
+ AbstractRobustConnection, AbstractQueue, AbstractExchange, AbstractMessage
6
+ )
7
+
8
+ from sycommon.logging.kafka_log import SYLogger
9
+
10
+ logger = SYLogger
11
+
12
+
13
+ class RabbitMQConnectionPool:
14
+ """单连接RabbitMQ通道池(严格单连接)"""
15
+
16
+ def __init__(
17
+ self,
18
+ hosts: List[str],
19
+ port: int,
20
+ username: str,
21
+ password: str,
22
+ virtualhost: str = "/",
23
+ channel_pool_size: int = 1,
24
+ heartbeat: int = 30,
25
+ app_name: str = "",
26
+ connection_timeout: int = 30,
27
+ reconnect_interval: int = 30,
28
+ prefetch_count: int = 2,
29
+ ):
30
+ self.hosts = [host.strip() for host in hosts if host.strip()]
31
+ if not self.hosts:
32
+ raise ValueError("至少需要提供一个RabbitMQ主机地址")
33
+
34
+ # 连接配置(所有通道共享此连接的配置)
35
+ self.port = port
36
+ self.username = username
37
+ self.password = password
38
+ self.virtualhost = virtualhost
39
+ self.app_name = app_name or "rabbitmq-client"
40
+ self.heartbeat = heartbeat
41
+ self.connection_timeout = connection_timeout
42
+ self.reconnect_interval = reconnect_interval
43
+ self.prefetch_count = prefetch_count
44
+ self.channel_pool_size = channel_pool_size
45
+
46
+ # 节点轮询:仅用于连接失效时切换节点(仍保持单连接)
47
+ self._host_iterator: Iterator[str] = self._create_host_iterator()
48
+ self._current_host: Optional[str] = None # 当前连接的节点
49
+
50
+ # 核心资源(严格单连接 + 通道池)
51
+ self._connection: Optional[AbstractRobustConnection] = None # 唯一连接
52
+ self._free_channels: List[Channel] = [] # 通道池(仅存储当前连接的通道)
53
+ self._used_channels: Set[Channel] = set()
54
+
55
+ # 状态控制(确保线程安全)
56
+ self._lock = asyncio.Lock()
57
+ self._initialized = False
58
+ self._is_shutdown = False
59
+ self._reconnecting = False # 避免重连并发冲突
60
+
61
+ def _create_host_iterator(self) -> Iterator[str]:
62
+ """创建节点轮询迭代器(无限循环,仅用于切换节点)"""
63
+ while True:
64
+ for host in self.hosts:
65
+ yield host
66
+
67
+ @property
68
+ def is_alive(self) -> bool:
69
+ """检查唯一连接是否存活(使用is_closed判断,兼容所有版本)"""
70
+ if not self._initialized or not self._connection:
71
+ return False
72
+ # 异步清理失效通道(不影响主流程)
73
+ asyncio.create_task(self._clean_invalid_channels())
74
+ return not self._connection.is_closed
75
+
76
+ async def _safe_close_resources(self):
77
+ """安全关闭资源:先关通道,再关连接(保证单连接特性)"""
78
+ async with self._lock:
79
+ # 1. 关闭所有通道(无论空闲还是使用中)
80
+ all_channels = self._free_channels + list(self._used_channels)
81
+ for channel in all_channels:
82
+ try:
83
+ if not channel.is_closed:
84
+ await channel.close()
85
+ except Exception as e:
86
+ logger.warning(f"关闭通道失败: {str(e)}")
87
+ self._free_channels.clear()
88
+ self._used_channels.clear()
89
+
90
+ # 2. 关闭唯一连接
91
+ if self._connection:
92
+ try:
93
+ if not self._connection.is_closed:
94
+ await self._connection.close()
95
+ logger.info(f"已关闭唯一连接: {self._current_host}:{self.port}")
96
+ except Exception as e:
97
+ logger.warning(f"关闭连接失败: {str(e)}")
98
+ self._connection = None # 置空,确保单连接
99
+
100
+ async def _create_single_connection(self) -> AbstractRobustConnection:
101
+ """创建唯一连接(失败时轮询节点,切换前关闭旧连接)"""
102
+ max_attempts = len(self.hosts) # 每个节点尝试1次
103
+ attempts = 0
104
+ last_error: Optional[Exception] = None
105
+
106
+ while attempts < max_attempts and not self._is_shutdown:
107
+ next_host = next(self._host_iterator)
108
+
109
+ # 切换节点前:强制关闭旧连接(保证单连接)
110
+ if self._connection:
111
+ await self._safe_close_resources()
112
+
113
+ self._current_host = next_host
114
+ conn_url = f"amqp://{self.username}:{self.password}@{self._current_host}:{self.port}/{self.virtualhost}"
115
+
116
+ try:
117
+ logger.info(f"尝试创建唯一连接: {self._current_host}:{self.port}")
118
+ conn = await connect_robust(
119
+ conn_url,
120
+ properties={
121
+ "connection_name": f"{self.app_name}_single_conn",
122
+ "product": self.app_name
123
+ },
124
+ heartbeat=self.heartbeat,
125
+ timeout=self.connection_timeout,
126
+ reconnect_interval=self.reconnect_interval,
127
+ max_reconnect_attempts=None, # 单节点内部自动重连
128
+ )
129
+ logger.info(f"唯一连接创建成功: {self._current_host}:{self.port}")
130
+ return conn
131
+ except Exception as e:
132
+ attempts += 1
133
+ last_error = e
134
+ logger.error(
135
+ f"连接节点 {self._current_host}:{self.port} 失败({attempts}/{max_attempts}): {str(e)}",
136
+ exc_info=True
137
+ )
138
+ await asyncio.sleep(30) # 避免频繁重试
139
+
140
+ raise ConnectionError(
141
+ f"所有节点创建唯一连接失败(节点列表: {self.hosts})"
142
+ ) from last_error
143
+
144
+ async def _init_channel_pool(self):
145
+ """初始化通道池(绑定到唯一连接,仅创建指定数量的通道)"""
146
+ if not self._connection or self._connection.is_closed:
147
+ raise RuntimeError("无有效连接,无法初始化通道池")
148
+
149
+ async with self._lock:
150
+ self._free_channels.clear()
151
+ self._used_channels.clear()
152
+
153
+ # 创建指定数量的通道(池大小由channel_pool_size控制)
154
+ for i in range(self.channel_pool_size):
155
+ try:
156
+ channel = await self._connection.channel()
157
+ await channel.set_qos(prefetch_count=self.prefetch_count)
158
+ self._free_channels.append(channel)
159
+ except Exception as e:
160
+ logger.error(f"创建通道失败(第{i+1}个): {str(e)}", exc_info=True)
161
+ # 通道创建失败不中断,继续创建剩余通道
162
+ continue
163
+
164
+ logger.info(
165
+ f"通道池初始化完成 - 连接: {self._current_host}:{self.port}, "
166
+ f"可用通道数: {len(self._free_channels)}/{self.channel_pool_size}"
167
+ )
168
+
169
+ async def _reconnect_if_needed(self) -> bool:
170
+ """连接失效时重连(保证单连接)"""
171
+ if self._is_shutdown or self._reconnecting:
172
+ return False
173
+
174
+ self._reconnecting = True
175
+ try:
176
+ logger.warning("连接失效,开始重连...")
177
+ # 重新创建唯一连接
178
+ self._connection = await self._create_single_connection()
179
+ # 重新初始化通道池
180
+ await self._init_channel_pool()
181
+ logger.info("重连成功,通道池已恢复")
182
+ return True
183
+ except Exception as e:
184
+ logger.error(f"重连失败: {str(e)}", exc_info=True)
185
+ self._initialized = False # 重连失败后标记未初始化
186
+ return False
187
+ finally:
188
+ self._reconnecting = False
189
+
190
+ async def _clean_invalid_channels(self):
191
+ """清理失效通道并补充(仅针对当前唯一连接)"""
192
+ if not self._connection:
193
+ return
194
+
195
+ async with self._lock:
196
+ # 1. 清理空闲通道中的失效通道
197
+ valid_free = [
198
+ chan for chan in self._free_channels if not chan.is_closed]
199
+ invalid_count = len(self._free_channels) - len(valid_free)
200
+ if invalid_count > 0:
201
+ logger.warning(f"清理{invalid_count}个失效空闲通道")
202
+ self._free_channels = valid_free
203
+
204
+ # 2. 清理使用中通道中的失效通道
205
+ valid_used = {
206
+ chan for chan in self._used_channels if not chan.is_closed}
207
+ invalid_used_count = len(self._used_channels) - len(valid_used)
208
+ if invalid_used_count > 0:
209
+ logger.warning(f"清理{invalid_used_count}个失效使用中通道")
210
+ self._used_channels = valid_used
211
+
212
+ # 3. 检查连接是否有效,无效则触发重连
213
+ if self._connection.is_closed:
214
+ await self._reconnect_if_needed()
215
+ return
216
+
217
+ # 4. 补充通道到指定大小(仅使用当前唯一连接创建)
218
+ total_valid = len(self._free_channels) + len(self._used_channels)
219
+ missing = self.channel_pool_size - total_valid
220
+ if missing > 0:
221
+ logger.info(f"通道池缺少{missing}个通道,补充中...")
222
+ for _ in range(missing):
223
+ try:
224
+ channel = await self._connection.channel()
225
+ await channel.set_qos(prefetch_count=self.prefetch_count)
226
+ self._free_channels.append(channel)
227
+ except Exception as e:
228
+ logger.error(f"补充通道失败: {str(e)}", exc_info=True)
229
+ break
230
+
231
+ async def init_pools(self):
232
+ """初始化:创建唯一连接 + 初始化通道池(仅执行一次)"""
233
+ if self._initialized:
234
+ logger.warning("通道池已初始化,无需重复调用")
235
+ return
236
+
237
+ if self._is_shutdown:
238
+ raise RuntimeError("通道池已关闭,无法初始化")
239
+
240
+ try:
241
+ # 1. 创建唯一连接
242
+ self._connection = await self._create_single_connection()
243
+ # 2. 初始化通道池(绑定到该连接)
244
+ await self._init_channel_pool()
245
+ self._initialized = True
246
+ logger.info("RabbitMQ单连接通道池初始化完成")
247
+ except Exception as e:
248
+ logger.error(f"初始化失败: {str(e)}", exc_info=True)
249
+ await self._safe_close_resources()
250
+ raise
251
+
252
+ async def acquire_channel(self) -> Tuple[Channel, AbstractRobustConnection]:
253
+ """获取通道(返回元组:(通道, 唯一连接),兼容上层代码)"""
254
+ if not self._initialized:
255
+ raise RuntimeError("通道池未初始化,请先调用init_pools()")
256
+
257
+ if self._is_shutdown:
258
+ raise RuntimeError("通道池已关闭,无法获取通道")
259
+
260
+ # 先清理失效通道,确保池内通道有效
261
+ await self._clean_invalid_channels()
262
+
263
+ async with self._lock:
264
+ # 优先从空闲池获取
265
+ if self._free_channels:
266
+ channel = self._free_channels.pop()
267
+ self._used_channels.add(channel)
268
+ # 返回(通道, 唯一连接)元组
269
+ return channel, self._connection
270
+
271
+ # 通道池已满,创建临时通道(超出池大小,用完关闭)
272
+ try:
273
+ if not self._connection or self._connection.is_closed:
274
+ raise RuntimeError("唯一连接已失效,无法创建临时通道")
275
+
276
+ channel = await self._connection.channel()
277
+ await channel.set_qos(prefetch_count=self.prefetch_count)
278
+ self._used_channels.add(channel)
279
+ logger.warning(
280
+ f"通道池已达上限({self.channel_pool_size}),创建临时通道(用完自动关闭)"
281
+ )
282
+ # 返回(通道, 唯一连接)元组
283
+ return channel, self._connection
284
+ except Exception as e:
285
+ logger.error(f"获取通道失败: {str(e)}", exc_info=True)
286
+ raise
287
+
288
+ async def release_channel(self, channel: Channel, conn: AbstractRobustConnection):
289
+ """释放通道(接收通道和连接参数,兼容上层代码)"""
290
+ if not channel or not conn or self._is_shutdown:
291
+ return
292
+
293
+ # 仅处理当前唯一连接的通道(避免无效连接的通道)
294
+ if conn != self._connection:
295
+ try:
296
+ await channel.close()
297
+ logger.warning("已关闭非当前连接的通道(可能是重连后的旧通道)")
298
+ except Exception as e:
299
+ logger.warning(f"关闭非当前连接通道失败: {str(e)}")
300
+ return
301
+
302
+ async with self._lock:
303
+ if channel not in self._used_channels:
304
+ return
305
+
306
+ self._used_channels.remove(channel)
307
+
308
+ # 仅归还:当前连接有效 + 通道未关闭 + 池未满
309
+ if (not self._connection.is_closed
310
+ and not channel.is_closed
311
+ and len(self._free_channels) < self.channel_pool_size):
312
+ self._free_channels.append(channel)
313
+ else:
314
+ # 无效通道直接关闭
315
+ try:
316
+ await channel.close()
317
+ except Exception as e:
318
+ logger.warning(f"关闭通道失败: {str(e)}")
319
+
320
+ async def declare_queue(self, queue_name: str, **kwargs) -> AbstractQueue:
321
+ """声明队列(使用池内通道,共享唯一连接)"""
322
+ channel, conn = await self.acquire_channel()
323
+ try:
324
+ return await channel.declare_queue(queue_name, **kwargs)
325
+ finally:
326
+ await self.release_channel(channel, conn)
327
+
328
+ async def declare_exchange(self, exchange_name: str, exchange_type: str = "direct", **kwargs) -> AbstractExchange:
329
+ """声明交换机(使用池内通道,共享唯一连接)"""
330
+ channel, conn = await self.acquire_channel()
331
+ try:
332
+ return await channel.declare_exchange(exchange_name, exchange_type, **kwargs)
333
+ finally:
334
+ await self.release_channel(channel, conn)
335
+
336
+ async def publish_message(self, routing_key: str, message_body: bytes, exchange_name: str = "", **kwargs):
337
+ """发布消息(使用池内通道,共享唯一连接)"""
338
+ channel, conn = await self.acquire_channel()
339
+ try:
340
+ exchange = channel.default_exchange if not exchange_name else await channel.get_exchange(exchange_name)
341
+ message = Message(body=message_body, **kwargs)
342
+ await exchange.publish(message, routing_key=routing_key)
343
+ logger.debug(
344
+ f"消息发布成功 - 节点: {self._current_host}, 交换机: {exchange.name}, 路由键: {routing_key}"
345
+ )
346
+ except Exception as e:
347
+ logger.error(f"发布消息失败: {str(e)}", exc_info=True)
348
+ raise
349
+ finally:
350
+ await self.release_channel(channel, conn)
351
+
352
+ async def consume_queue(self, queue_name: str, callback, auto_ack: bool = False, **kwargs):
353
+ """消费队列(使用池内通道,共享唯一连接)"""
354
+ if not self._initialized:
355
+ raise RuntimeError("通道池未初始化,请先调用init_pools()")
356
+
357
+ queue = await self.declare_queue(queue_name, **kwargs)
358
+ current_channel, current_conn = await self.acquire_channel() # 元组解包
359
+
360
+ async def consume_callback_wrapper(message: AbstractMessage):
361
+ """消费回调包装(处理通道失效重连)"""
362
+ nonlocal current_channel, current_conn
363
+ try:
364
+ # 检查通道是否有效(连接可能已切换)
365
+ if (current_channel.is_closed
366
+ or current_conn.is_closed
367
+ or current_conn != self._connection):
368
+ logger.warning("消费通道失效,重新获取通道...")
369
+ await self.release_channel(current_channel, current_conn)
370
+ current_channel, current_conn = await self.acquire_channel()
371
+ return
372
+
373
+ await callback(message)
374
+ if not auto_ack:
375
+ await message.ack()
376
+ except Exception as e:
377
+ logger.error(f"消费消息失败: {str(e)}", exc_info=True)
378
+ if not auto_ack:
379
+ await message.nack(requeue=True)
380
+
381
+ logger.info(f"开始消费队列: {queue_name}(连接节点: {self._current_host})")
382
+ try:
383
+ async with queue.iterator() as queue_iter:
384
+ async for message in queue_iter:
385
+ if self._is_shutdown:
386
+ logger.info("消费已停止,退出消费循环")
387
+ break
388
+ await consume_callback_wrapper(message)
389
+ finally:
390
+ await self.release_channel(current_channel, current_conn)
391
+
392
+ async def close(self):
393
+ """关闭通道池:释放所有通道 + 关闭唯一连接"""
394
+ if self._is_shutdown:
395
+ logger.warning("通道池已关闭,无需重复操作")
396
+ return
397
+
398
+ self._is_shutdown = True
399
+ logger.info("开始关闭RabbitMQ单连接通道池...")
400
+
401
+ # 安全释放所有资源
402
+ await self._safe_close_resources()
403
+
404
+ logger.info("RabbitMQ单连接通道池已完全关闭")