sycommon-python-lib 0.2.0b2__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.
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/PKG-INFO +1 -1
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/pyproject.toml +1 -1
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_client.py +149 -105
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_pool.py +46 -81
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_client_manager.py +7 -17
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +1 -1
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +2 -2
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_producer_manager.py +8 -8
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/README.md +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/setup.cfg +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/command/cli.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/Config.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/DatabaseConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/EmbeddingConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/LLMConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/LangfuseConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/MQConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/RerankerConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/SentryConfig.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/async_base_db_service.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/async_database_service.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/base_db_service.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/database_service.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/health_check.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/metrics.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/ping.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/embedding.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/get_llm.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/llm_logger.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/llm_tokens.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/struct_token.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/sy_langfuse.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/usage_token.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/async_sql_logger.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/kafka_log.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/logger_levels.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/logger_wrapper.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/sql_logger.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/context.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/cors.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/docs.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/exception.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/middleware.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/monitor_memory.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/mq.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/timeout.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/traceid.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/base_http.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/log.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqlistener_config.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqmsg_model.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqsend_config.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/sso_user.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/notice/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/notice/uvicorn_monitor.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_service_core.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/sentry/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/sentry/sy_sentry.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/services.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/sse/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/sse/event.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/sse/sse.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/example.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/example2.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/feign.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/feign_client.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_client_base.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_config_manager.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_heartbeat_manager.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service_discovery.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service_registration.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/param.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tests/test_email.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/__init__.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/docs.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/env.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/merge_headers.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/snowflake.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/syemail.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/timing.py +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
- {sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_client.py
RENAMED
|
@@ -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,34 +80,46 @@ class RabbitMQClient:
|
|
|
78
80
|
self._RECONNECT_INTERVAL = 15
|
|
79
81
|
|
|
80
82
|
@property
|
|
81
|
-
|
|
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.
|
|
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:
|
|
92
104
|
return False
|
|
93
105
|
|
|
94
106
|
async def _rebuild_resources(self) -> None:
|
|
107
|
+
"""重建 Exchange 和 Queue 资源"""
|
|
95
108
|
if not self._channel or self._channel.is_closed:
|
|
96
109
|
raise RuntimeError("无有效通道,无法重建资源")
|
|
97
110
|
|
|
98
|
-
#
|
|
99
|
-
|
|
111
|
+
# 1. 无条件声明交换机
|
|
112
|
+
exchange_inst = await self._channel.declare_exchange(
|
|
100
113
|
name=self.exchange_name,
|
|
101
114
|
type=self.exchange_type,
|
|
102
115
|
durable=self.durable,
|
|
103
116
|
auto_delete=self.auto_delete,
|
|
104
117
|
passive=not self.create_if_not_exists,
|
|
105
118
|
)
|
|
119
|
+
self._exchange = exchange_inst
|
|
106
120
|
logger.info(f"交换机重建成功: {self.exchange_name}")
|
|
107
121
|
|
|
108
|
-
#
|
|
122
|
+
# 2. 仅在有队列名且符合条件时声明队列
|
|
109
123
|
if self.queue_name and self.queue_name.endswith(f".{self.app_name}"):
|
|
110
124
|
self._queue = await self._channel.declare_queue(
|
|
111
125
|
name=self.queue_name,
|
|
@@ -113,61 +127,76 @@ class RabbitMQClient:
|
|
|
113
127
|
auto_delete=self.auto_delete,
|
|
114
128
|
passive=not self.create_if_not_exists,
|
|
115
129
|
)
|
|
116
|
-
await self._queue.bind(exchange=
|
|
130
|
+
await self._queue.bind(exchange=exchange_inst, routing_key=self.routing_key)
|
|
117
131
|
logger.info(f"队列重建成功: {self.queue_name}")
|
|
118
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
|
+
|
|
119
147
|
async def connect(self) -> None:
|
|
120
|
-
"""
|
|
148
|
+
"""连接方法(终极防御版:防并发风暴、防死锁、防资源泄漏)"""
|
|
121
149
|
if self._closed:
|
|
122
150
|
raise RuntimeError("客户端已关闭,无法重新连接")
|
|
123
151
|
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if self._connect_condition.locked():
|
|
131
|
-
self._connect_condition.release()
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
# 如果已有协程正在连接,等待其完成
|
|
135
|
-
if self._connecting:
|
|
136
|
-
try:
|
|
137
|
-
logger.debug("连接正在进行中,等待现有连接完成...")
|
|
138
|
-
await asyncio.wait_for(self._connect_condition.wait(), timeout=60.0)
|
|
139
|
-
except asyncio.TimeoutError:
|
|
140
|
-
logger.warning("等待前序连接超时,当前协程将尝试强制接管并重连...")
|
|
141
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
152
178
|
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
# 3. 抢到连接权,标记开始
|
|
180
|
+
self._connecting = True
|
|
155
181
|
self._connect_condition.release()
|
|
156
|
-
|
|
182
|
+
break # <--- 跳出循环,去执行连接逻辑
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
if self._connect_condition.locked():
|
|
186
|
+
self._connect_condition.release()
|
|
187
|
+
raise
|
|
157
188
|
|
|
158
189
|
# === 阶段 C: 执行耗时的连接逻辑 (无锁状态) ===
|
|
159
190
|
connection_failed = False
|
|
160
191
|
was_consuming = False
|
|
161
|
-
|
|
162
|
-
# 判断当前是否为消费者模式(通过是否有消息处理函数判断)
|
|
163
192
|
is_consumer = self._message_handler is not None
|
|
164
193
|
old_channel = self._channel
|
|
165
194
|
|
|
166
195
|
try:
|
|
167
|
-
# --- 步骤 1:
|
|
196
|
+
# --- 步骤 1: 清理旧资源 ---
|
|
168
197
|
was_consuming = self._consumer_tag is not None
|
|
169
198
|
|
|
170
|
-
#
|
|
199
|
+
# 清理旧连接的回调
|
|
171
200
|
if self._channel_conn:
|
|
172
201
|
try:
|
|
173
202
|
if self._channel_conn.close_callbacks:
|
|
@@ -175,56 +204,57 @@ class RabbitMQClient:
|
|
|
175
204
|
except Exception:
|
|
176
205
|
pass
|
|
177
206
|
|
|
178
|
-
# 显式关闭旧 Channel
|
|
179
|
-
# 注意:无论是生产者复用的主通道,还是消费者的独立通道,断开时都应显式关闭以释放服务端资源
|
|
180
207
|
if old_channel and not old_channel.is_closed:
|
|
181
208
|
try:
|
|
182
209
|
await old_channel.close()
|
|
183
210
|
except Exception:
|
|
184
211
|
pass
|
|
185
212
|
|
|
186
|
-
#
|
|
187
|
-
# 因为我们即将获取一个新的 Channel,旧的 Exchange 和 Queue 对象(基于旧 Channel)将全部失效。
|
|
188
|
-
# 必须置为 None,强制后续逻辑基于新 Channel 重建这些对象。
|
|
213
|
+
# 强制重置
|
|
189
214
|
self._channel = None
|
|
190
215
|
self._channel_conn = None
|
|
191
216
|
self._exchange = None
|
|
192
217
|
self._queue = None
|
|
193
218
|
self._consumer_tag = None
|
|
194
219
|
|
|
195
|
-
# --- 步骤 2:
|
|
196
|
-
# 生产者:复用连接池的主通道(性能高)
|
|
197
|
-
# 消费者:从连接池获取独立的通道(稳定性高,避免并发冲突)
|
|
220
|
+
# --- 步骤 2: 获取新通道 ---
|
|
198
221
|
if is_consumer:
|
|
199
222
|
logger.debug("获取消费者独立通道...")
|
|
200
223
|
self._channel = await self.connection_pool.acquire_consumer_channel()
|
|
201
|
-
self._channel_conn = self.connection_pool._connection
|
|
202
224
|
else:
|
|
203
225
|
logger.debug("获取生产者主通道...")
|
|
204
226
|
self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
|
|
205
227
|
|
|
206
|
-
# --- 步骤 3:
|
|
228
|
+
# --- 步骤 3: 统一获取并注册连接回调 ---
|
|
229
|
+
if not self._channel_conn:
|
|
230
|
+
# 消费者路径需要手动获取连接引用
|
|
231
|
+
# 直接引用连接池的连接对象,确保引用时效性
|
|
232
|
+
self._channel_conn = self.connection_pool._connection
|
|
233
|
+
|
|
207
234
|
loop = asyncio.get_running_loop()
|
|
208
235
|
|
|
209
236
|
def on_conn_closed(conn, exc):
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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()
|
|
215
247
|
|
|
216
|
-
|
|
217
|
-
self._channel_conn.close_callbacks.add(on_conn_closed)
|
|
248
|
+
self._channel_conn.close_callbacks.add(on_conn_closed)
|
|
218
249
|
|
|
219
250
|
# --- 步骤 4: 重建基础资源 ---
|
|
220
|
-
# 这会在新的 self._channel 上声明 Exchange 和 Queue,并执行绑定
|
|
221
251
|
await self._rebuild_resources()
|
|
222
252
|
|
|
223
253
|
except Exception as e:
|
|
224
254
|
connection_failed = True
|
|
225
|
-
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
255
|
+
logger.error(f"❌ 客户端连接失败: {str(e)}", exc_info=True)
|
|
226
256
|
|
|
227
|
-
#
|
|
257
|
+
# 异常时清理
|
|
228
258
|
if self._channel_conn and self._channel_conn.close_callbacks:
|
|
229
259
|
self._channel_conn.close_callbacks.clear()
|
|
230
260
|
|
|
@@ -233,31 +263,25 @@ class RabbitMQClient:
|
|
|
233
263
|
self._exchange = None
|
|
234
264
|
self._queue = None
|
|
235
265
|
self._consumer_tag = None
|
|
236
|
-
|
|
237
266
|
raise
|
|
238
267
|
|
|
239
268
|
finally:
|
|
240
|
-
# === 阶段 D: 恢复消费与收尾
|
|
269
|
+
# === 阶段 D: 恢复消费与收尾 ===
|
|
241
270
|
try:
|
|
242
271
|
await self._connect_condition.acquire()
|
|
243
272
|
except Exception:
|
|
244
|
-
|
|
273
|
+
# 如果 acquire 本身失败,确保状态复位,防止死锁
|
|
274
|
+
self._connecting = False
|
|
275
|
+
self._connect_condition.notify_all()
|
|
276
|
+
raise
|
|
245
277
|
|
|
246
278
|
try:
|
|
247
|
-
# 只有连接完全成功,且之前处于消费状态,才尝试自动恢复消费
|
|
248
279
|
if not connection_failed and was_consuming and self._message_handler:
|
|
249
280
|
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
|
|
250
281
|
|
|
251
|
-
# 【修复核心】
|
|
252
|
-
# 由于在步骤 1 中 self._queue 已被置为 None,
|
|
253
|
-
# 如果 _rebuild_resources 因为某种原因(例如配置条件)没有成功创建队列,
|
|
254
|
-
# 这里需要再次尝试在当前新 Channel 上创建并绑定队列。
|
|
255
|
-
# 不再检查 is_closed(因为该属性不存在),直接检查是否为 None。
|
|
256
|
-
|
|
257
282
|
if self.queue_name and not self._queue:
|
|
258
283
|
try:
|
|
259
284
|
logger.info(f"重连恢复过程中重新声明队列: {self.queue_name}")
|
|
260
|
-
# 在当前新 Channel 上声明队列
|
|
261
285
|
self._queue = await self._channel.declare_queue(
|
|
262
286
|
name=self.queue_name,
|
|
263
287
|
durable=self.durable,
|
|
@@ -265,18 +289,25 @@ class RabbitMQClient:
|
|
|
265
289
|
passive=not self.create_if_not_exists,
|
|
266
290
|
)
|
|
267
291
|
|
|
268
|
-
#
|
|
269
|
-
# 即使队列已存在,也必须在新 Channel 上重新绑定,否则服务端路由状态可能不更新
|
|
292
|
+
# 确保绑定
|
|
270
293
|
if self._exchange:
|
|
271
294
|
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
272
295
|
logger.info(
|
|
273
296
|
f"✅ 重连绑定成功: {self.queue_name} -> {self.routing_key}")
|
|
297
|
+
else:
|
|
298
|
+
# 防御性编程
|
|
299
|
+
logger.error(
|
|
300
|
+
"🔥 Exchange missing, forcing declare...")
|
|
301
|
+
temp_ex = await self._channel.declare_exchange(
|
|
302
|
+
self.exchange_name, self.exchange_type, durable=self.durable
|
|
303
|
+
)
|
|
304
|
+
self._exchange = temp_ex
|
|
305
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
306
|
+
|
|
274
307
|
except Exception as bind_err:
|
|
275
308
|
logger.error(f"❌ 重连恢复队列/绑定失败: {bind_err}")
|
|
276
|
-
# 绑定失败,无法恢复消费
|
|
277
309
|
self._queue = None
|
|
278
310
|
|
|
279
|
-
# 队列对象有效才启动消费
|
|
280
311
|
if self._queue:
|
|
281
312
|
try:
|
|
282
313
|
self._consumer_tag = await self.start_consuming()
|
|
@@ -284,41 +315,35 @@ class RabbitMQClient:
|
|
|
284
315
|
except Exception as e:
|
|
285
316
|
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
286
317
|
self._consumer_tag = None
|
|
287
|
-
else:
|
|
288
|
-
logger.warning("⚠️ 队列对象无效,无法恢复消费")
|
|
289
|
-
|
|
290
318
|
finally:
|
|
291
|
-
#
|
|
319
|
+
# 无论恢复逻辑成功与否,必须解锁
|
|
292
320
|
self._connecting = False
|
|
293
321
|
self._connect_condition.notify_all()
|
|
294
|
-
|
|
295
322
|
if self._connect_condition.locked():
|
|
296
323
|
self._connect_condition.release()
|
|
297
324
|
|
|
298
325
|
async def _safe_reconnect(self):
|
|
299
|
-
"""
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
logger.info(f"将在{self._RECONNECT_INTERVAL}秒后尝试重连...")
|
|
309
|
-
await asyncio.sleep(self._RECONNECT_INTERVAL)
|
|
310
|
-
|
|
311
|
-
if self._closed or await self.is_connected:
|
|
312
|
-
return
|
|
326
|
+
"""
|
|
327
|
+
安全重连入口
|
|
328
|
+
【核心修复】使用锁防止并发风暴,确保同一时间只有一个重连任务在执行。
|
|
329
|
+
"""
|
|
330
|
+
# 如果锁已经被占用,说明已经有重连任务在进行中,直接忽略,防止无限递归
|
|
331
|
+
if self._reconnect_lock.locked():
|
|
332
|
+
logger.debug("⏳ 重连任务已在执行中,忽略本次触发,避免并发风暴")
|
|
333
|
+
return
|
|
313
334
|
|
|
335
|
+
async with self._reconnect_lock:
|
|
314
336
|
try:
|
|
315
|
-
self.
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
if self._closed:
|
|
338
|
+
logger.info("客户端已手动关闭,取消重连")
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
logger.warning("🔄 触发底层连接重连...")
|
|
342
|
+
# 调用实际的 connect 方法
|
|
343
|
+
await self.connect()
|
|
318
344
|
except Exception as e:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
self._current_reconnect_task = None
|
|
345
|
+
# 即使重连失败,也不要在这里递归调用自己,而是依赖外部的定时任务或下一次网络事件
|
|
346
|
+
logger.error(f"❌ 安全重连执行失败: {str(e)}", exc_info=True)
|
|
322
347
|
|
|
323
348
|
async def set_message_handler(self, handler: Callable[..., Coroutine]) -> None:
|
|
324
349
|
if not asyncio.iscoroutinefunction(handler):
|
|
@@ -327,23 +352,42 @@ class RabbitMQClient:
|
|
|
327
352
|
self._message_handler = handler
|
|
328
353
|
|
|
329
354
|
async def _process_message_callback(self, message: AbstractIncomingMessage):
|
|
355
|
+
"""
|
|
356
|
+
消息处理回调
|
|
357
|
+
"""
|
|
330
358
|
try:
|
|
359
|
+
# === 阶段 1: 消息解析与上下文设置 (在主线程执行) ===
|
|
331
360
|
body_dict = json.loads(message.body.decode("utf-8"))
|
|
332
361
|
msg_obj: MQMsgModel = MQMsgModel(**body_dict)
|
|
362
|
+
|
|
333
363
|
if not msg_obj.traceId:
|
|
334
364
|
msg_obj.traceId = message.headers.get(
|
|
335
365
|
"trace-id") if message.headers else SYLogger.get_trace_id()
|
|
336
366
|
|
|
337
367
|
SYLogger.set_trace_id(msg_obj.traceId)
|
|
338
368
|
|
|
369
|
+
# === 阶段 2: 业务逻辑执行 ===
|
|
339
370
|
if self._message_handler:
|
|
340
|
-
|
|
371
|
+
def run_job_with_context():
|
|
372
|
+
# 【关键修复】在子线程内重新设置 TraceId
|
|
373
|
+
SYLogger.set_trace_id(msg_obj.traceId)
|
|
341
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)
|
|
381
|
+
|
|
382
|
+
# === 阶段 3: 消息确认 ===
|
|
342
383
|
await message.ack()
|
|
343
384
|
|
|
344
385
|
except Exception as e:
|
|
345
|
-
logger.error(f"消息处理异常: {e}", exc_info=True)
|
|
346
|
-
|
|
386
|
+
logger.error(f"❌ 消息处理异常: {str(e)}", exc_info=True)
|
|
387
|
+
try:
|
|
388
|
+
await message.ack()
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
347
391
|
|
|
348
392
|
async def start_consuming(self) -> Optional[ConsumerTag]:
|
|
349
393
|
if self._closed:
|
|
@@ -353,7 +397,7 @@ class RabbitMQClient:
|
|
|
353
397
|
if not self._message_handler:
|
|
354
398
|
raise RuntimeError("未设置消息处理器")
|
|
355
399
|
|
|
356
|
-
if not
|
|
400
|
+
if not self.is_connected:
|
|
357
401
|
await self.connect()
|
|
358
402
|
|
|
359
403
|
if not self._queue:
|
|
@@ -431,7 +475,7 @@ class RabbitMQClient:
|
|
|
431
475
|
|
|
432
476
|
for retry in range(retry_count):
|
|
433
477
|
try:
|
|
434
|
-
if not
|
|
478
|
+
if not self.is_connected:
|
|
435
479
|
await self.connect()
|
|
436
480
|
|
|
437
481
|
result = await self._exchange.publish(
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/rabbitmq/rabbitmq_pool.py
RENAMED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
@@ -29,29 +29,17 @@ class RabbitMQClientManager(RabbitMQCoreService):
|
|
|
29
29
|
@classmethod
|
|
30
30
|
async def _clean_client_resources(cls, client: RabbitMQClient) -> None:
|
|
31
31
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
重要变更:
|
|
35
|
-
- 移除了 await client._channel.close() 调用。
|
|
36
|
-
- 原因:关闭 Channel 可能会触发底层连接的 close_callback,导致重连死锁或冲突。
|
|
37
|
-
- Channel 的关闭应该由 RabbitMQClient.connect() 内部的重建逻辑接管。
|
|
32
|
+
清理客户端无效资源
|
|
38
33
|
"""
|
|
39
34
|
try:
|
|
40
|
-
# 1.
|
|
35
|
+
# 1. 停止消费
|
|
41
36
|
if client._consumer_tag:
|
|
42
37
|
await client.stop_consuming()
|
|
43
38
|
except Exception as e:
|
|
44
39
|
logger.warning(f"停止消费逻辑异常: {str(e)}")
|
|
45
40
|
|
|
46
|
-
# 注意:这里不再显式关闭 client._channel
|
|
47
|
-
# 原因:
|
|
48
|
-
# 1. 如果是消费者独立通道,关闭它是安全的,但不如交给 connect() 统一处理(重建前关闭)。
|
|
49
|
-
# 2. 如果是生产者主通道(由 Pool 缓存),绝对不能在这里关闭!否则会影响其他生产者。
|
|
50
|
-
# 3. connect() 方法已经包含了 "显式关闭旧 Channel" 的逻辑,这里重复操作是多余的且危险的。
|
|
51
|
-
|
|
52
41
|
try:
|
|
53
|
-
# 2.
|
|
54
|
-
# 注意:虽然清理了状态,但不要清空 _message_handler!
|
|
42
|
+
# 2. 重置引用
|
|
55
43
|
client._channel = None
|
|
56
44
|
client._channel_conn = None
|
|
57
45
|
client._exchange = None
|
|
@@ -59,6 +47,8 @@ class RabbitMQClientManager(RabbitMQCoreService):
|
|
|
59
47
|
client._consumer_tag = None
|
|
60
48
|
except Exception as e:
|
|
61
49
|
logger.warning(f"重置客户端状态异常: {str(e)}")
|
|
50
|
+
# 注意:不要在这里关闭 _channel,交给 client.close() 或者重连逻辑处理
|
|
51
|
+
# 这里主要确保引用断开,帮助 GC
|
|
62
52
|
|
|
63
53
|
@classmethod
|
|
64
54
|
async def _reconnect_client(cls, client_name: str, client: RabbitMQClient) -> bool:
|
|
@@ -95,7 +85,7 @@ class RabbitMQClientManager(RabbitMQCoreService):
|
|
|
95
85
|
await client.connect()
|
|
96
86
|
|
|
97
87
|
# 验证重连结果
|
|
98
|
-
if
|
|
88
|
+
if client.is_connected:
|
|
99
89
|
logger.info(f"✅ 客户端 '{client_name}' 重连成功")
|
|
100
90
|
return True
|
|
101
91
|
else:
|
|
@@ -209,7 +199,7 @@ class RabbitMQClientManager(RabbitMQCoreService):
|
|
|
209
199
|
if not client.queue_name and kwargs.get("queue_name"):
|
|
210
200
|
client.queue_name = kwargs.get("queue_name")
|
|
211
201
|
|
|
212
|
-
if
|
|
202
|
+
if client.is_connected:
|
|
213
203
|
return client
|
|
214
204
|
else:
|
|
215
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/DatabaseConfig.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/EmbeddingConfig.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/LLMConfig.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/LangfuseConfig.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/RerankerConfig.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/config/SentryConfig.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/database/base_db_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/health/health_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/llm/struct_token.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/__init__.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/async_sql_logger.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/kafka_log.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/logger_levels.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/logger_wrapper.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/logging/sql_logger.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/__init__.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/exception.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/timeout.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/middleware/traceid.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/base_http.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqlistener_config.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqmsg_model.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/models/mqsend_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/notice/uvicorn_monitor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/sentry/sy_sentry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/example2.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/feign_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/synacos/nacos_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tests/test_email.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.2.0b2 → sycommon_python_lib-0.2.0b4}/src/sycommon/tools/merge_headers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|