sycommon-python-lib 0.1.56b11__py3-none-any.whl → 0.1.56b12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sycommon/rabbitmq/rabbitmq_client.py +224 -290
- sycommon/rabbitmq/rabbitmq_pool.py +182 -154
- sycommon/rabbitmq/rabbitmq_service_core.py +2 -2
- {sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/RECORD +8 -8
- {sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
from aio_pika import Channel
|
|
2
|
-
from typing import Optional
|
|
3
1
|
import asyncio
|
|
4
2
|
import json
|
|
5
|
-
from typing import Callable, Coroutine, Dict, Any, Union
|
|
6
|
-
from aio_pika import Message, DeliveryMode, ExchangeType
|
|
3
|
+
from typing import Optional, Callable, Coroutine, Dict, Any, Union
|
|
4
|
+
from aio_pika import Channel, Message, DeliveryMode, ExchangeType
|
|
7
5
|
from aio_pika.abc import (
|
|
8
6
|
AbstractExchange,
|
|
9
7
|
AbstractQueue,
|
|
@@ -20,13 +18,7 @@ logger = SYLogger
|
|
|
20
18
|
|
|
21
19
|
class RabbitMQClient:
|
|
22
20
|
"""
|
|
23
|
-
RabbitMQ
|
|
24
|
-
核心特性:
|
|
25
|
-
1. 基于单通道连接池复用资源,性能优化
|
|
26
|
-
2. 依赖连接池原生自动重连,客户端仅重建自身资源
|
|
27
|
-
3. 消息发布支持重试+mandatory机制+超时控制,确保路由有效
|
|
28
|
-
4. 消费支持手动ACK/NACK
|
|
29
|
-
5. 兼容JSON/字符串/字典消息格式
|
|
21
|
+
RabbitMQ 客户端
|
|
30
22
|
"""
|
|
31
23
|
|
|
32
24
|
def __init__(
|
|
@@ -42,12 +34,10 @@ class RabbitMQClient:
|
|
|
42
34
|
create_if_not_exists: bool = True,
|
|
43
35
|
**kwargs,
|
|
44
36
|
):
|
|
45
|
-
# 依赖注入:连接池(必须已初始化)
|
|
46
37
|
self.connection_pool = connection_pool
|
|
47
38
|
if not self.connection_pool._initialized:
|
|
48
39
|
raise RuntimeError("连接池未初始化,请先调用 connection_pool.init_pools()")
|
|
49
40
|
|
|
50
|
-
# 交换机配置
|
|
51
41
|
self.exchange_name = exchange_name.strip()
|
|
52
42
|
try:
|
|
53
43
|
self.exchange_type = ExchangeType(exchange_type.lower())
|
|
@@ -55,17 +45,16 @@ class RabbitMQClient:
|
|
|
55
45
|
logger.warning(f"无效的exchange_type: {exchange_type},默认使用'topic'")
|
|
56
46
|
self.exchange_type = ExchangeType.TOPIC
|
|
57
47
|
|
|
58
|
-
# 队列配置
|
|
59
48
|
self.queue_name = queue_name.strip() if queue_name else None
|
|
60
49
|
self.routing_key = routing_key.strip() if routing_key else "#"
|
|
61
|
-
self.durable = durable
|
|
62
|
-
self.auto_delete = auto_delete
|
|
63
|
-
self.auto_parse_json = auto_parse_json
|
|
64
|
-
self.create_if_not_exists = create_if_not_exists
|
|
50
|
+
self.durable = durable
|
|
51
|
+
self.auto_delete = auto_delete
|
|
52
|
+
self.auto_parse_json = auto_parse_json
|
|
53
|
+
self.create_if_not_exists = create_if_not_exists
|
|
65
54
|
|
|
66
|
-
#
|
|
55
|
+
# 资源状态
|
|
67
56
|
self._channel: Optional[Channel] = None
|
|
68
|
-
self._channel_conn: Optional[AbstractRobustConnection] = None
|
|
57
|
+
self._channel_conn: Optional[AbstractRobustConnection] = None
|
|
69
58
|
self._exchange: Optional[AbstractExchange] = None
|
|
70
59
|
self._queue: Optional[AbstractQueue] = None
|
|
71
60
|
self._consumer_tag: Optional[ConsumerTag] = None
|
|
@@ -73,45 +62,38 @@ class RabbitMQClient:
|
|
|
73
62
|
MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]]] = None
|
|
74
63
|
self._closed = False
|
|
75
64
|
|
|
76
|
-
#
|
|
65
|
+
# 并发控制
|
|
77
66
|
self._consume_lock = asyncio.Lock()
|
|
78
67
|
self._connect_lock = asyncio.Lock()
|
|
79
|
-
|
|
68
|
+
|
|
69
|
+
# 防止并发重连覆盖
|
|
70
|
+
self._connecting = False
|
|
71
|
+
self._connect_condition = asyncio.Condition()
|
|
72
|
+
|
|
80
73
|
self._conn_close_callback: Optional[Callable] = None
|
|
81
|
-
# 控制重连频率的信号量(限制并发重连数)
|
|
82
74
|
self._reconnect_semaphore = asyncio.Semaphore(1)
|
|
83
|
-
# 固定重连间隔15秒(全局统一)
|
|
84
|
-
self._RECONNECT_INTERVAL = 15
|
|
85
|
-
# 跟踪当前重连任务(避免重复创建)
|
|
86
75
|
self._current_reconnect_task: Optional[asyncio.Task] = None
|
|
87
|
-
|
|
88
|
-
self._reconnect_fail_count = 0
|
|
89
|
-
# 连接失败告警阈值
|
|
90
|
-
self._reconnect_alert_threshold = 5
|
|
76
|
+
self._RECONNECT_INTERVAL = 15
|
|
91
77
|
|
|
92
78
|
@property
|
|
93
79
|
async def is_connected(self) -> bool:
|
|
94
|
-
"""异步检查客户端连接状态(属性,不可调用)"""
|
|
95
80
|
if self._closed:
|
|
96
81
|
return False
|
|
97
82
|
try:
|
|
98
|
-
# 单通道场景:校验通道+连接+核心资源都有效
|
|
99
83
|
return (
|
|
100
84
|
self._channel and not self._channel.is_closed
|
|
101
85
|
and self._channel_conn and not self._channel_conn.is_closed
|
|
102
86
|
and self._exchange is not None
|
|
103
87
|
and (not self.queue_name or self._queue is not None)
|
|
104
88
|
)
|
|
105
|
-
except Exception
|
|
106
|
-
logger.warning(f"检查连接状态失败: {str(e)}")
|
|
89
|
+
except Exception:
|
|
107
90
|
return False
|
|
108
91
|
|
|
109
92
|
async def _rebuild_resources(self) -> None:
|
|
110
|
-
"""重建交换机/队列等资源(依赖已有的通道)"""
|
|
111
93
|
if not self._channel or self._channel.is_closed:
|
|
112
94
|
raise RuntimeError("无有效通道,无法重建资源")
|
|
113
95
|
|
|
114
|
-
#
|
|
96
|
+
# 声明交换机
|
|
115
97
|
self._exchange = await self._channel.declare_exchange(
|
|
116
98
|
name=self.exchange_name,
|
|
117
99
|
type=self.exchange_type,
|
|
@@ -119,10 +101,9 @@ class RabbitMQClient:
|
|
|
119
101
|
auto_delete=self.auto_delete,
|
|
120
102
|
passive=not self.create_if_not_exists,
|
|
121
103
|
)
|
|
122
|
-
logger.info(
|
|
123
|
-
f"交换机重建成功: {self.exchange_name}(类型: {self.exchange_type.value})")
|
|
104
|
+
logger.info(f"交换机重建成功: {self.exchange_name}")
|
|
124
105
|
|
|
125
|
-
#
|
|
106
|
+
# 声明队列
|
|
126
107
|
if self.queue_name:
|
|
127
108
|
self._queue = await self._channel.declare_queue(
|
|
128
109
|
name=self.queue_name,
|
|
@@ -130,100 +111,134 @@ class RabbitMQClient:
|
|
|
130
111
|
auto_delete=self.auto_delete,
|
|
131
112
|
passive=not self.create_if_not_exists,
|
|
132
113
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
exchange=self._exchange,
|
|
136
|
-
routing_key=self.routing_key,
|
|
137
|
-
)
|
|
138
|
-
logger.info(
|
|
139
|
-
f"队列重建成功: {self.queue_name} "
|
|
140
|
-
f"(绑定交换机: {self.exchange_name}, routing_key: {self.routing_key})"
|
|
141
|
-
)
|
|
114
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
115
|
+
logger.info(f"队列重建成功: {self.queue_name}")
|
|
142
116
|
|
|
143
117
|
async def connect(self) -> None:
|
|
144
118
|
if self._closed:
|
|
145
119
|
raise RuntimeError("客户端已关闭,无法重新连接")
|
|
146
120
|
|
|
121
|
+
# 1. 并发控制:使用 _connect_lock 保证只有一个协程在执行连接流程
|
|
147
122
|
async with self._connect_lock:
|
|
148
|
-
#
|
|
149
|
-
if self.
|
|
123
|
+
# 如果已经在连了,等待其完成
|
|
124
|
+
if self._connecting:
|
|
125
|
+
logger.debug("连接正在进行中,等待现有连接完成...")
|
|
150
126
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
127
|
+
# 等待条件变量,超时设为 60 秒防止死等
|
|
128
|
+
await asyncio.wait_for(
|
|
129
|
+
self._connect_condition.wait_for(
|
|
130
|
+
lambda: not self._connecting),
|
|
131
|
+
timeout=60.0
|
|
132
|
+
)
|
|
133
|
+
except asyncio.TimeoutError:
|
|
134
|
+
raise RuntimeError("等待连接超时")
|
|
155
135
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
self._queue = None
|
|
161
|
-
self._conn_close_callback = None
|
|
136
|
+
# 等待结束后,再次检查状态
|
|
137
|
+
if not await self.is_connected:
|
|
138
|
+
raise RuntimeError("等待重连后,连接状态依然无效")
|
|
139
|
+
return
|
|
162
140
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
|
|
166
|
-
|
|
167
|
-
# 4. 设置新连接回调(使用 weakref)
|
|
168
|
-
def on_conn_closed(conn: AbstractRobustConnection, exc: Optional[BaseException]):
|
|
169
|
-
# 注意:这里需要访问外部的 self,使用闭包或 weakref
|
|
170
|
-
# 简单起见,这里用闭包,但务必在 self.close 或 self.connect 时清理回调
|
|
171
|
-
logger.warning(f"检测到连接关闭: {exc}")
|
|
172
|
-
if not self._closed:
|
|
173
|
-
asyncio.create_task(self._safe_reconnect())
|
|
174
|
-
|
|
175
|
-
self._conn_close_callback = on_conn_closed
|
|
176
|
-
if self._channel_conn:
|
|
177
|
-
self._channel_conn.close_callbacks.add(
|
|
178
|
-
self._conn_close_callback)
|
|
141
|
+
# 标记开始连接
|
|
142
|
+
self._connecting = True
|
|
179
143
|
|
|
180
|
-
|
|
181
|
-
|
|
144
|
+
# 释放 _connect_lock,允许其他协程读取状态,但在连接完成前阻止新的连接请求
|
|
145
|
+
# 注意:这里释放了 _connect_lock,但 self._connecting = True 阻止了新的连接流程
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# --- 阶段1: 清理旧资源 ---
|
|
149
|
+
# 重新获取锁进行资源清理
|
|
150
|
+
async with self._connect_lock:
|
|
151
|
+
was_consuming = self._consumer_tag is not None
|
|
182
152
|
|
|
183
|
-
# 重置计数
|
|
184
|
-
self._reconnect_fail_count = 0
|
|
185
|
-
logger.info("客户端连接初始化完成")
|
|
186
|
-
except Exception as e:
|
|
187
|
-
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
188
|
-
# 失败时也要清理可能产生的残留引用
|
|
189
153
|
if self._channel_conn and self._conn_close_callback:
|
|
190
154
|
try:
|
|
191
155
|
self._channel_conn.close_callbacks.discard(
|
|
192
156
|
self._conn_close_callback)
|
|
193
157
|
except Exception:
|
|
194
158
|
pass
|
|
195
|
-
|
|
159
|
+
|
|
196
160
|
self._channel = None
|
|
197
161
|
self._channel_conn = None
|
|
162
|
+
self._exchange = None
|
|
163
|
+
self._queue = None
|
|
198
164
|
self._conn_close_callback = None
|
|
199
165
|
|
|
200
|
-
|
|
201
|
-
|
|
166
|
+
# --- 阶段2: 获取新连接 (耗时IO) ---
|
|
167
|
+
self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
|
|
168
|
+
|
|
169
|
+
# 设置回调
|
|
170
|
+
def on_conn_closed(conn, exc):
|
|
171
|
+
logger.warning(f"检测到连接关闭: {exc}")
|
|
172
|
+
if not self._closed and not self._connecting:
|
|
202
173
|
asyncio.create_task(self._safe_reconnect())
|
|
203
|
-
|
|
174
|
+
|
|
175
|
+
self._conn_close_callback = on_conn_closed
|
|
176
|
+
if self._channel_conn:
|
|
177
|
+
self._channel_conn.close_callbacks.add(
|
|
178
|
+
self._conn_close_callback)
|
|
179
|
+
|
|
180
|
+
# 重建资源
|
|
181
|
+
await self._rebuild_resources()
|
|
182
|
+
|
|
183
|
+
# --- 阶段3: 恢复消费 ---
|
|
184
|
+
if was_consuming and self._message_handler and self.queue_name:
|
|
185
|
+
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复...")
|
|
186
|
+
try:
|
|
187
|
+
self._queue = await self._channel.declare_queue(
|
|
188
|
+
name=self.queue_name,
|
|
189
|
+
durable=self.durable,
|
|
190
|
+
auto_delete=self.auto_delete,
|
|
191
|
+
passive=False,
|
|
192
|
+
)
|
|
193
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
194
|
+
self._consumer_tag = await self._queue.consume(self._process_message_callback)
|
|
195
|
+
logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
198
|
+
self._consumer_tag = None
|
|
199
|
+
else:
|
|
200
|
+
self._consumer_tag = None
|
|
201
|
+
|
|
202
|
+
logger.info("客户端连接初始化完成")
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
206
|
+
|
|
207
|
+
# 异常时清理资源
|
|
208
|
+
async with self._connect_lock:
|
|
209
|
+
if self._channel_conn and self._conn_close_callback:
|
|
210
|
+
self._channel_conn.close_callbacks.discard(
|
|
211
|
+
self._conn_close_callback)
|
|
212
|
+
self._channel = None
|
|
213
|
+
self._channel_conn = None
|
|
214
|
+
self._consumer_tag = None
|
|
215
|
+
|
|
216
|
+
raise
|
|
217
|
+
|
|
218
|
+
finally:
|
|
219
|
+
# 【关键修复】必须在持有 Condition 内部锁的情况下调用 notify_all
|
|
220
|
+
# 这里使用 async with self._connect_condition: 自动完成 acquire() ... notify_all() ... release()
|
|
221
|
+
async with self._connect_condition:
|
|
222
|
+
self._connecting = False
|
|
223
|
+
self._connect_condition.notify_all()
|
|
204
224
|
|
|
205
225
|
async def _safe_reconnect(self):
|
|
206
|
-
"""
|
|
226
|
+
"""安全重连任务(仅用于被动监听连接关闭)"""
|
|
207
227
|
async with self._reconnect_semaphore:
|
|
208
|
-
|
|
209
|
-
if self._current_reconnect_task and not self._current_reconnect_task.done():
|
|
210
|
-
logger.debug("已有重连任务在运行,跳过重复触发")
|
|
228
|
+
if self._closed:
|
|
211
229
|
return
|
|
212
230
|
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
# 如果已经在重连,直接忽略
|
|
232
|
+
if self._connecting:
|
|
215
233
|
return
|
|
216
234
|
|
|
217
|
-
# 固定15秒重连间隔
|
|
218
235
|
logger.info(f"将在{self._RECONNECT_INTERVAL}秒后尝试重连...")
|
|
219
236
|
await asyncio.sleep(self._RECONNECT_INTERVAL)
|
|
220
237
|
|
|
221
238
|
if self._closed or await self.is_connected:
|
|
222
|
-
logger.debug("重连等待期间客户端状态变化,取消重连")
|
|
223
239
|
return
|
|
224
240
|
|
|
225
241
|
try:
|
|
226
|
-
logger.info("开始重连RabbitMQ客户端...")
|
|
227
242
|
self._current_reconnect_task = asyncio.create_task(
|
|
228
243
|
self.connect())
|
|
229
244
|
await self._current_reconnect_task
|
|
@@ -232,163 +247,112 @@ class RabbitMQClient:
|
|
|
232
247
|
finally:
|
|
233
248
|
self._current_reconnect_task = None
|
|
234
249
|
|
|
235
|
-
async def set_message_handler(
|
|
236
|
-
self,
|
|
237
|
-
handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]],
|
|
238
|
-
) -> None:
|
|
239
|
-
"""设置消息处理器(必须是协程函数)"""
|
|
250
|
+
async def set_message_handler(self, handler: Callable[..., Coroutine]) -> None:
|
|
240
251
|
if not asyncio.iscoroutinefunction(handler):
|
|
241
|
-
raise TypeError("
|
|
242
|
-
|
|
252
|
+
raise TypeError("消息处理器必须是协程函数")
|
|
243
253
|
async with self._consume_lock:
|
|
244
254
|
self._message_handler = handler
|
|
245
|
-
|
|
255
|
+
|
|
256
|
+
async def _process_message_callback(self, message: AbstractIncomingMessage):
|
|
257
|
+
try:
|
|
258
|
+
msg_obj: MQMsgModel
|
|
259
|
+
if self.auto_parse_json:
|
|
260
|
+
try:
|
|
261
|
+
body_dict = json.loads(message.body.decode("utf-8"))
|
|
262
|
+
msg_obj = MQMsgModel(**body_dict)
|
|
263
|
+
except json.JSONDecodeError as e:
|
|
264
|
+
logger.error(f"JSON解析失败: {e}")
|
|
265
|
+
await message.nack(requeue=False)
|
|
266
|
+
return
|
|
267
|
+
else:
|
|
268
|
+
msg_obj = MQMsgModel(
|
|
269
|
+
body=message.body.decode("utf-8"),
|
|
270
|
+
routing_key=message.routing_key,
|
|
271
|
+
delivery_tag=message.delivery_tag,
|
|
272
|
+
traceId=message.headers.get("trace-id"),
|
|
273
|
+
headers=message.headers
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
SYLogger.set_trace_id(msg_obj.traceId)
|
|
277
|
+
|
|
278
|
+
if self._message_handler:
|
|
279
|
+
await self._message_handler(msg_obj, message)
|
|
280
|
+
|
|
281
|
+
await message.ack()
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"消息处理异常: {e}", exc_info=True)
|
|
285
|
+
headers = dict(message.headers) if message.headers else {}
|
|
286
|
+
current_retry = int(headers.get("x-retry-count", 0))
|
|
287
|
+
|
|
288
|
+
if current_retry >= 3:
|
|
289
|
+
logger.warning(f"重试次数超限,丢弃消息: {message.delivery_tag}")
|
|
290
|
+
await message.reject(requeue=False)
|
|
291
|
+
else:
|
|
292
|
+
headers["x-retry-count"] = current_retry + 1
|
|
293
|
+
try:
|
|
294
|
+
new_msg = Message(
|
|
295
|
+
body=message.body,
|
|
296
|
+
headers=headers,
|
|
297
|
+
content_type=message.content_type,
|
|
298
|
+
delivery_mode=message.delivery_mode
|
|
299
|
+
)
|
|
300
|
+
# 这里的 publish 如果失败,会触发重连机制
|
|
301
|
+
# 但注意,当前是在回调线程中,建议做好异常捕获
|
|
302
|
+
await self._exchange.publish(new_msg, routing_key=message.routing_key)
|
|
303
|
+
await message.ack()
|
|
304
|
+
except Exception as pub_err:
|
|
305
|
+
logger.error(f"重试发布失败: {pub_err}")
|
|
306
|
+
await message.reject(requeue=False)
|
|
246
307
|
|
|
247
308
|
async def start_consuming(self) -> Optional[ConsumerTag]:
|
|
248
|
-
"""启动消息消费(支持自动重连 + Header 重试计数限制)"""
|
|
249
309
|
if self._closed:
|
|
250
310
|
raise RuntimeError("客户端已关闭,无法启动消费")
|
|
251
311
|
|
|
252
312
|
async with self._consume_lock:
|
|
253
|
-
# 1. 校验前置条件
|
|
254
313
|
if not self._message_handler:
|
|
255
|
-
raise RuntimeError("
|
|
314
|
+
raise RuntimeError("未设置消息处理器")
|
|
315
|
+
|
|
256
316
|
if not await self.is_connected:
|
|
257
317
|
await self.connect()
|
|
258
|
-
if not self._queue:
|
|
259
|
-
raise RuntimeError("未配置队列名或队列未创建,无法启动消费")
|
|
260
|
-
|
|
261
|
-
# 2. 定义消费回调
|
|
262
|
-
async def consume_callback(message: AbstractIncomingMessage):
|
|
263
|
-
try:
|
|
264
|
-
# 解析消息体
|
|
265
|
-
msg_obj: MQMsgModel
|
|
266
|
-
if self.auto_parse_json:
|
|
267
|
-
try:
|
|
268
|
-
body_dict = json.loads(
|
|
269
|
-
message.body.decode("utf-8"))
|
|
270
|
-
msg_obj = MQMsgModel(**body_dict)
|
|
271
|
-
except json.JSONDecodeError as e:
|
|
272
|
-
logger.error(
|
|
273
|
-
f"JSON消息解析失败: {str(e)},消息体: {message.body[:100]}...")
|
|
274
|
-
# 解析失败通常无法重试,直接丢弃
|
|
275
|
-
await message.nack(requeue=False)
|
|
276
|
-
return
|
|
277
|
-
else:
|
|
278
|
-
msg_obj = MQMsgModel(
|
|
279
|
-
body=message.body.decode("utf-8"),
|
|
280
|
-
routing_key=message.routing_key,
|
|
281
|
-
delivery_tag=message.delivery_tag,
|
|
282
|
-
traceId=message.headers.get("trace-id", None),
|
|
283
|
-
headers=message.headers
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
# 统一追踪ID
|
|
287
|
-
SYLogger.set_trace_id(
|
|
288
|
-
message.headers.get("trace-id", None))
|
|
289
|
-
|
|
290
|
-
# 调用消息处理器
|
|
291
|
-
await self._message_handler(msg_obj, message)
|
|
292
|
-
|
|
293
|
-
# 处理成功,手动ACK
|
|
294
|
-
await message.ack()
|
|
295
|
-
logger.debug(
|
|
296
|
-
f"消息处理成功,delivery_tag: {message.delivery_tag}")
|
|
297
318
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
current_retry_count = 0
|
|
306
|
-
if message.headers:
|
|
307
|
-
current_retry_count = int(
|
|
308
|
-
message.headers.get("x-retry-count", 0))
|
|
309
|
-
|
|
310
|
-
# 2. 检查是否超过最大重试次数 (3次)
|
|
311
|
-
MAX_RETRY = 3
|
|
312
|
-
if current_retry_count >= MAX_RETRY:
|
|
313
|
-
logger.warning(
|
|
314
|
-
f"消息重试次数已达上限({MAX_RETRY}),丢弃消息。"
|
|
315
|
-
f"delivery_tag: {message.delivery_tag}, routing_key: {message.routing_key}"
|
|
316
|
-
)
|
|
317
|
-
# 丢弃消息(不重新入队)
|
|
318
|
-
await message.reject(requeue=False)
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
# 3. 次数未满,准备重入队
|
|
322
|
-
# 注意:AbstractIncomingMessage.headers 是只读的 (frozendict),
|
|
323
|
-
# 不能直接 message.headers["x-retry-count"] = ...
|
|
324
|
-
# 必须重新构造 headers 字典。
|
|
325
|
-
new_headers = dict(
|
|
326
|
-
message.headers) if message.headers else {}
|
|
327
|
-
new_headers["x-retry-count"] = current_retry_count + 1
|
|
328
|
-
|
|
329
|
-
logger.warning(
|
|
330
|
-
f"消息处理失败,准备第 {current_retry_count + 1} 次重入队。"
|
|
331
|
-
f"delivery_tag: {message.delivery_tag}"
|
|
319
|
+
if not self._queue:
|
|
320
|
+
if self.queue_name:
|
|
321
|
+
self._queue = await self._channel.declare_queue(
|
|
322
|
+
name=self.queue_name,
|
|
323
|
+
durable=self.durable,
|
|
324
|
+
auto_delete=self.auto_delete,
|
|
325
|
+
passive=not self.create_if_not_exists,
|
|
332
326
|
)
|
|
327
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
328
|
+
else:
|
|
329
|
+
raise RuntimeError("未配置队列名")
|
|
333
330
|
|
|
334
|
-
|
|
335
|
-
new_msg = Message(
|
|
336
|
-
body=message.body,
|
|
337
|
-
headers=new_headers,
|
|
338
|
-
content_type=message.content_type,
|
|
339
|
-
delivery_mode=message.delivery_mode
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# 发布到原 Exchange 和 RoutingKey
|
|
343
|
-
# 注意:这里可能会抛出异常,如果抛异常,消息可能会丢失(因为已经 Nack 掉了)
|
|
344
|
-
await self._exchange.publish(
|
|
345
|
-
new_msg,
|
|
346
|
-
routing_key=message.routing_key
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# 发布成功后,Ack 掉旧消息
|
|
350
|
-
await message.ack()
|
|
351
|
-
logger.info(
|
|
352
|
-
f"消息已重新发布 (重试次数: {new_headers['x-retry-count']})")
|
|
353
|
-
|
|
354
|
-
except Exception as publish_err:
|
|
355
|
-
logger.error(f"重新发布消息失败(消息丢失): {publish_err}")
|
|
356
|
-
# 重新发布失败,只能丢弃或者 Nack(False)
|
|
357
|
-
await message.reject(requeue=False)
|
|
358
|
-
|
|
359
|
-
# 3. 启动消费(单通道消费,避免阻塞发布需确保业务回调非阻塞)
|
|
360
|
-
self._consumer_tag = await self._queue.consume(consume_callback)
|
|
331
|
+
self._consumer_tag = await self._queue.consume(self._process_message_callback)
|
|
361
332
|
logger.info(
|
|
362
|
-
f"开始消费队列: {self._queue.name},
|
|
363
|
-
)
|
|
333
|
+
f"开始消费队列: {self._queue.name},tag: {self._consumer_tag}")
|
|
364
334
|
return self._consumer_tag
|
|
365
335
|
|
|
366
336
|
async def stop_consuming(self) -> None:
|
|
367
|
-
"""停止消息消费(适配 RobustChannel)"""
|
|
368
337
|
async with self._consume_lock:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if self._consumer_tag and self._queue and self._channel and not self._channel.is_closed:
|
|
372
|
-
# 使用队列的 cancel 方法(适配 RobustChannel)
|
|
338
|
+
if self._consumer_tag and self._queue and self._channel:
|
|
339
|
+
try:
|
|
373
340
|
await self._queue.cancel(self._consumer_tag)
|
|
374
|
-
logger.info(
|
|
375
|
-
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
390
|
-
finally:
|
|
391
|
-
self._consumer_tag = None # 无论成败,清理消费标签
|
|
341
|
+
logger.info(f"停止消费成功: {self._consumer_tag}")
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.warning(f"停止消费异常: {e}")
|
|
344
|
+
self._consumer_tag = None
|
|
345
|
+
|
|
346
|
+
async def _handle_publish_failure(self):
|
|
347
|
+
try:
|
|
348
|
+
logger.info("检测到发布异常,强制连接池切换节点...")
|
|
349
|
+
await self.connection_pool.force_reconnect()
|
|
350
|
+
# 连接池切换后,必须刷新客户端资源
|
|
351
|
+
await self.connect()
|
|
352
|
+
logger.info("故障转移完成,资源已刷新")
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f"故障转移失败: {e}")
|
|
355
|
+
raise
|
|
392
356
|
|
|
393
357
|
async def publish(
|
|
394
358
|
self,
|
|
@@ -398,18 +362,9 @@ class RabbitMQClient:
|
|
|
398
362
|
delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT,
|
|
399
363
|
retry_count: int = 3,
|
|
400
364
|
) -> None:
|
|
401
|
-
"""
|
|
402
|
-
发布消息(支持自动重试、mandatory路由校验、5秒超时控制)
|
|
403
|
-
:param message_body: 消息体(字符串/字典/MQMsgModel)
|
|
404
|
-
:param headers: 消息头(可选)
|
|
405
|
-
:param content_type: 内容类型(默认application/json)
|
|
406
|
-
:param delivery_mode: 投递模式(PERSISTENT=持久化,TRANSIENT=非持久化)
|
|
407
|
-
:param retry_count: 重试次数(默认3次)
|
|
408
|
-
"""
|
|
409
365
|
if self._closed:
|
|
410
366
|
raise RuntimeError("客户端已关闭,无法发布消息")
|
|
411
367
|
|
|
412
|
-
# 处理消息体序列化
|
|
413
368
|
try:
|
|
414
369
|
if isinstance(message_body, MQMsgModel):
|
|
415
370
|
body = json.dumps(message_body.to_dict(),
|
|
@@ -422,93 +377,72 @@ class RabbitMQClient:
|
|
|
422
377
|
else:
|
|
423
378
|
raise TypeError(f"不支持的消息体类型: {type(message_body)}")
|
|
424
379
|
except Exception as e:
|
|
425
|
-
logger.error(f"消息体序列化失败: {
|
|
380
|
+
logger.error(f"消息体序列化失败: {e}")
|
|
426
381
|
raise
|
|
427
382
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
headers=headers or {},
|
|
432
|
-
content_type=content_type,
|
|
433
|
-
delivery_mode=delivery_mode,
|
|
434
|
-
)
|
|
383
|
+
message = Message(body=body, headers=headers or {},
|
|
384
|
+
content_type=content_type, delivery_mode=delivery_mode)
|
|
385
|
+
last_exception = None
|
|
435
386
|
|
|
436
|
-
# 发布重试逻辑
|
|
437
387
|
for retry in range(retry_count):
|
|
438
388
|
try:
|
|
439
|
-
# 确保连接有效
|
|
440
389
|
if not await self.is_connected:
|
|
441
|
-
logger.warning(f"发布消息前连接失效,触发重连(retry: {retry})")
|
|
442
390
|
await self.connect()
|
|
443
391
|
|
|
444
|
-
|
|
445
|
-
publish_result = await self._exchange.publish(
|
|
392
|
+
result = await self._exchange.publish(
|
|
446
393
|
message=message,
|
|
447
394
|
routing_key=self.routing_key or self.queue_name or "#",
|
|
448
395
|
mandatory=True,
|
|
449
396
|
timeout=5.0
|
|
450
397
|
)
|
|
451
398
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
raise RuntimeError(
|
|
455
|
-
f"消息未找到匹配的队列(routing_key: {self.routing_key}),mandatory=True 触发失败"
|
|
456
|
-
)
|
|
399
|
+
if result is None:
|
|
400
|
+
raise RuntimeError(f"消息未找到匹配的队列: {self.routing_key}")
|
|
457
401
|
|
|
458
|
-
logger.info(
|
|
459
|
-
f"消息发布成功(retry: {retry}),routing_key: {self.routing_key},"
|
|
460
|
-
f"delivery_mode: {delivery_mode.value},mandatory: True,timeout: 5.0s"
|
|
461
|
-
)
|
|
402
|
+
logger.info(f"发布成功: {self.routing_key}")
|
|
462
403
|
return
|
|
463
|
-
|
|
464
|
-
logger.error(
|
|
465
|
-
f"消息发布超时(retry: {retry}/{retry_count-1}),超时时间: 5.0s"
|
|
466
|
-
)
|
|
404
|
+
|
|
467
405
|
except RuntimeError as e:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
)
|
|
406
|
+
if "未找到匹配的队列" in str(e):
|
|
407
|
+
raise
|
|
408
|
+
last_exception = str(e)
|
|
409
|
+
await self._handle_publish_failure()
|
|
410
|
+
|
|
471
411
|
except Exception as e:
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)
|
|
476
|
-
# 清理失效状态,下次重试重连
|
|
477
|
-
self._exchange = None
|
|
478
|
-
# 指数退避重试间隔
|
|
479
|
-
await asyncio.sleep(0.5 * (2 ** retry))
|
|
412
|
+
last_exception = str(e)
|
|
413
|
+
logger.error(f"发布异常: {e}")
|
|
414
|
+
await self._handle_publish_failure()
|
|
480
415
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
f"mandatory: True,timeout: 5.0s"
|
|
485
|
-
)
|
|
416
|
+
await asyncio.sleep(5)
|
|
417
|
+
|
|
418
|
+
raise RuntimeError(f"消息发布最终失败: {last_exception}")
|
|
486
419
|
|
|
487
420
|
async def close(self) -> None:
|
|
488
|
-
"""关闭客户端(移除回调+释放资源)"""
|
|
489
421
|
self._closed = True
|
|
490
422
|
logger.info("开始关闭RabbitMQ客户端...")
|
|
491
423
|
|
|
492
|
-
# 停止重连任务
|
|
493
424
|
if self._current_reconnect_task and not self._current_reconnect_task.done():
|
|
494
425
|
self._current_reconnect_task.cancel()
|
|
495
426
|
try:
|
|
496
427
|
await self._current_reconnect_task
|
|
497
428
|
except asyncio.CancelledError:
|
|
498
|
-
|
|
429
|
+
pass
|
|
499
430
|
|
|
500
|
-
# 1. 停止消费
|
|
501
431
|
await self.stop_consuming()
|
|
502
432
|
|
|
503
|
-
# 2. 清理回调+状态(单通道无需归还,连接池统一管理)
|
|
504
433
|
async with self._connect_lock:
|
|
505
434
|
if self._conn_close_callback and self._channel_conn:
|
|
506
435
|
self._channel_conn.close_callbacks.discard(
|
|
507
436
|
self._conn_close_callback)
|
|
437
|
+
|
|
508
438
|
self._channel = None
|
|
509
439
|
self._channel_conn = None
|
|
510
440
|
self._exchange = None
|
|
511
441
|
self._queue = None
|
|
512
442
|
self._message_handler = None
|
|
513
443
|
|
|
514
|
-
|
|
444
|
+
# 确保唤醒可能正在等待 connect 的任务
|
|
445
|
+
self._connecting = False
|
|
446
|
+
self._connect_condition.notify_all()
|
|
447
|
+
|
|
448
|
+
logger.info("客户端已关闭")
|
|
@@ -19,12 +19,11 @@ class AsyncProperty:
|
|
|
19
19
|
def __get__(self, obj, objtype=None):
|
|
20
20
|
if obj is None:
|
|
21
21
|
return self
|
|
22
|
-
# 关键:当访问 obj.attr 时,直接返回协程对象,而不是方法本身
|
|
23
22
|
return self.method(obj)
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class RabbitMQConnectionPool:
|
|
27
|
-
"""单连接单通道RabbitMQ客户端 (
|
|
26
|
+
"""单连接单通道RabbitMQ客户端 (严格执行“先清理后连接”策略)"""
|
|
28
27
|
|
|
29
28
|
def __init__(
|
|
30
29
|
self,
|
|
@@ -33,9 +32,9 @@ class RabbitMQConnectionPool:
|
|
|
33
32
|
username: str,
|
|
34
33
|
password: str,
|
|
35
34
|
virtualhost: str = "/",
|
|
36
|
-
heartbeat: int =
|
|
35
|
+
heartbeat: int = 15,
|
|
37
36
|
app_name: str = "",
|
|
38
|
-
connection_timeout: int =
|
|
37
|
+
connection_timeout: int = 15,
|
|
39
38
|
reconnect_interval: int = 5,
|
|
40
39
|
prefetch_count: int = 2,
|
|
41
40
|
):
|
|
@@ -68,174 +67,231 @@ class RabbitMQConnectionPool:
|
|
|
68
67
|
|
|
69
68
|
@AsyncProperty
|
|
70
69
|
async def is_alive(self) -> bool:
|
|
71
|
-
"""
|
|
70
|
+
"""对外暴露的连接存活状态"""
|
|
72
71
|
async with self._lock:
|
|
73
72
|
if self._is_shutdown:
|
|
74
73
|
return False
|
|
75
|
-
|
|
76
74
|
if not self._initialized:
|
|
77
75
|
return False
|
|
78
|
-
|
|
79
76
|
if self._connection is None or self._connection.is_closed:
|
|
80
77
|
return False
|
|
81
|
-
|
|
82
|
-
# 可选:检查主通道是否存活
|
|
83
78
|
if self._channel is None or self._channel.is_closed:
|
|
84
|
-
# 如果你认为通道断了连接也算死,就保留这行;否则删除
|
|
85
79
|
return False
|
|
86
|
-
|
|
87
80
|
return True
|
|
88
81
|
|
|
89
|
-
async def
|
|
82
|
+
async def _cleanup_resources(self):
|
|
90
83
|
"""
|
|
91
|
-
|
|
84
|
+
彻底清理旧资源
|
|
85
|
+
必须在持有 self._lock 的情况下调用
|
|
92
86
|
"""
|
|
87
|
+
logger.info("🧹 [CLEANUP] 开始清理旧资源...")
|
|
88
|
+
|
|
89
|
+
# 1. 清理所有消费者通道
|
|
90
|
+
if self._consumer_channels:
|
|
91
|
+
channels_to_close = list(self._consumer_channels.values())
|
|
92
|
+
self._consumer_channels.clear()
|
|
93
|
+
|
|
94
|
+
for ch in channels_to_close:
|
|
95
|
+
try:
|
|
96
|
+
if not ch.is_closed:
|
|
97
|
+
await ch.close()
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(f"⚠️ [CLEANUP_CH] 关闭消费者通道失败: {e}")
|
|
100
|
+
|
|
101
|
+
# 2. 关闭主通道
|
|
102
|
+
if self._channel:
|
|
103
|
+
try:
|
|
104
|
+
if not self._channel.is_closed:
|
|
105
|
+
await self._channel.close()
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(f"⚠️ [CLEANUP_MAIN_CH] 关闭主通道失败: {e}")
|
|
108
|
+
finally:
|
|
109
|
+
self._channel = None
|
|
110
|
+
|
|
111
|
+
# 3. 关闭连接
|
|
112
|
+
if self._connection:
|
|
113
|
+
try:
|
|
114
|
+
if not self._connection.is_closed:
|
|
115
|
+
# close() 可能是同步的,也可能是异步的,aio_pika 中通常是异步的
|
|
116
|
+
await self._connection.close()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.warning(f"⚠️ [CLEANUP_CONN] 关闭连接失败: {e}")
|
|
119
|
+
finally:
|
|
120
|
+
self._connection = None
|
|
121
|
+
|
|
122
|
+
logger.info("✅ [CLEANUP] 资源清理完成")
|
|
123
|
+
|
|
124
|
+
async def _create_connection_impl(self, host: str) -> AbstractRobustConnection:
|
|
93
125
|
conn_url = (
|
|
94
|
-
f"amqp://{self.username}:{self.password}@{
|
|
126
|
+
f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
|
|
95
127
|
f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
|
|
96
128
|
f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
|
|
97
129
|
)
|
|
98
|
-
logger.info(
|
|
99
|
-
f"🔌 [CONNECT_START] 尝试创建连接 -> {self._current_host}:{self.port}")
|
|
130
|
+
logger.info(f"🔌 [CONNECT] 尝试连接节点: {host}")
|
|
100
131
|
try:
|
|
101
|
-
conn = await
|
|
102
|
-
|
|
103
|
-
|
|
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}")
|
|
104
137
|
return conn
|
|
105
138
|
except Exception as e:
|
|
106
|
-
logger.error(f"❌ [CONNECT_FAIL]
|
|
107
|
-
raise ConnectionError(f"无法连接RabbitMQ {
|
|
108
|
-
|
|
109
|
-
async def _create_channel_impl(self, connection: AbstractRobustConnection) -> RobustChannel:
|
|
110
|
-
"""创建通道"""
|
|
111
|
-
try:
|
|
112
|
-
channel = await connection.channel()
|
|
113
|
-
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
114
|
-
logger.debug(f"✅ [CHANNEL_OK] 通道创建成功: {id(channel)}")
|
|
115
|
-
return channel
|
|
116
|
-
except Exception as e:
|
|
117
|
-
logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败: {str(e)}")
|
|
118
|
-
raise
|
|
139
|
+
logger.error(f"❌ [CONNECT_FAIL] 节点 {host} 连接失败: {str(e)}")
|
|
140
|
+
raise ConnectionError(f"无法连接RabbitMQ {host}") from e
|
|
119
141
|
|
|
120
142
|
async def _ensure_main_channel(self) -> RobustChannel:
|
|
121
|
-
"""
|
|
143
|
+
"""
|
|
144
|
+
确保主通道有效
|
|
145
|
+
逻辑:
|
|
146
|
+
1. 检查连接状态
|
|
147
|
+
2. 如果断开 -> 清理 -> 轮询重试
|
|
148
|
+
3. 如果连接在但通道断开 -> 仅重建通道
|
|
149
|
+
"""
|
|
122
150
|
async with self._lock:
|
|
123
151
|
if self._is_shutdown:
|
|
124
152
|
raise RuntimeError("客户端已关闭")
|
|
125
153
|
|
|
126
|
-
#
|
|
154
|
+
# --- 阶段A:连接恢复逻辑 (如果连接断了) ---
|
|
127
155
|
if self._connection is None or self._connection.is_closed:
|
|
128
|
-
|
|
156
|
+
|
|
157
|
+
# 1. 【强制】先彻底清理所有旧资源
|
|
158
|
+
await self._cleanup_resources()
|
|
159
|
+
|
|
160
|
+
retry_hosts = self.hosts.copy()
|
|
161
|
+
random.shuffle(retry_hosts)
|
|
129
162
|
last_error = None
|
|
130
|
-
|
|
163
|
+
max_attempts = min(len(retry_hosts), 3)
|
|
131
164
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
# 2. 轮询尝试新连接
|
|
166
|
+
for _ in range(max_attempts):
|
|
167
|
+
if not retry_hosts:
|
|
168
|
+
break
|
|
136
169
|
|
|
137
|
-
|
|
170
|
+
host = retry_hosts.pop()
|
|
138
171
|
self._current_host = host
|
|
139
|
-
temp_conn = None
|
|
172
|
+
temp_conn = None
|
|
140
173
|
|
|
141
174
|
try:
|
|
142
|
-
|
|
143
|
-
f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
|
|
144
|
-
f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
|
|
145
|
-
f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
# 尝试连接
|
|
149
|
-
temp_conn = await connect_robust(conn_url, timeout=self.connection_timeout)
|
|
175
|
+
temp_conn = await self._create_connection_impl(host)
|
|
150
176
|
|
|
151
|
-
#
|
|
177
|
+
# 3. 只有在连接成功后,才更新 self._connection
|
|
152
178
|
self._connection = temp_conn
|
|
153
|
-
temp_conn = None #
|
|
154
|
-
|
|
155
|
-
logger.info(f"🔗 [RECONNECT_OK] 成功连接到节点: {host}")
|
|
179
|
+
temp_conn = None # 转移所有权
|
|
180
|
+
self._initialized = True
|
|
156
181
|
last_error = None
|
|
157
|
-
|
|
182
|
+
logger.info(f"🔗 [RECONNECT_OK] 切换到节点: {host}")
|
|
183
|
+
break
|
|
158
184
|
|
|
159
185
|
except Exception as e:
|
|
160
|
-
logger.warning(
|
|
161
|
-
f"❌ [RECONNECT_FAIL] 节点 {host} 不可用: {str(e)}")
|
|
162
|
-
|
|
163
|
-
# 【核心修复】清理失败的连接对象
|
|
186
|
+
logger.warning(f"⚠️ [RECONNECT_RETRY] 节点 {host} 不可用")
|
|
164
187
|
if temp_conn is not None:
|
|
188
|
+
# 尝试连接失败了,必须把这个“半成品”连接关掉
|
|
165
189
|
try:
|
|
166
|
-
logger.debug(
|
|
167
|
-
f"🧹 [CLEANUP] 正在关闭失败的连接对象: {id(temp_conn)}")
|
|
168
|
-
# 即使连接对象处于异常状态,close() 通常也是安全的
|
|
169
190
|
await temp_conn.close()
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
logger.warning(
|
|
173
|
-
f"⚠️ [CLEANUP_ERR] 关闭失败连接时出错: {str(close_err)}")
|
|
174
|
-
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
175
193
|
last_error = e
|
|
176
|
-
asyncio.sleep(self.reconnect_interval)
|
|
177
|
-
continue # 继续试下一个
|
|
194
|
+
await asyncio.sleep(self.reconnect_interval)
|
|
178
195
|
|
|
179
|
-
#
|
|
196
|
+
# 4. 如果所有尝试都失败
|
|
180
197
|
if last_error:
|
|
181
|
-
|
|
198
|
+
# 确保状态是干净的
|
|
199
|
+
self._connection = None
|
|
200
|
+
self._initialized = False
|
|
201
|
+
logger.error("💥 [RECONNECT_FATAL] 所有节点重试失败")
|
|
182
202
|
raise ConnectionError("所有 RabbitMQ 节点连接失败") from last_error
|
|
183
203
|
|
|
184
|
-
#
|
|
204
|
+
# --- 阶段B:通道恢复逻辑 (如果连接在但通道断了) ---
|
|
205
|
+
# 注意:这里不需要清理连接,只重置通道
|
|
185
206
|
if self._channel is None or self._channel.is_closed:
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
try:
|
|
208
|
+
self._channel = await self._connection.channel()
|
|
209
|
+
await self._channel.set_qos(prefetch_count=self.prefetch_count)
|
|
210
|
+
logger.info(f"✅ [CHANNEL_OK] 主通道已恢复")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# 如果连通道都创建不了,说明这个连接也是坏的,回滚到阶段A
|
|
213
|
+
logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败,标记连接无效: {e}")
|
|
214
|
+
# 强制清理连接,触发下一次进入阶段A
|
|
215
|
+
await self._cleanup_resources()
|
|
216
|
+
raise
|
|
188
217
|
|
|
189
218
|
return self._channel
|
|
190
219
|
|
|
191
220
|
async def init_pools(self):
|
|
192
|
-
"""
|
|
193
|
-
初始化入口与异常处理 (修复泄漏的关键)
|
|
194
|
-
"""
|
|
221
|
+
"""初始化入口"""
|
|
195
222
|
async with self._lock:
|
|
196
223
|
if self._is_shutdown:
|
|
197
224
|
raise RuntimeError("客户端已关闭")
|
|
198
225
|
if self._initialized:
|
|
199
226
|
return
|
|
200
227
|
|
|
228
|
+
# 在 try 之前声明变量,确保 except 块能访问
|
|
201
229
|
conn_created_in_this_try = None
|
|
230
|
+
|
|
202
231
|
try:
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
|
|
232
|
+
# 锁外创建连接,减少锁持有时间
|
|
233
|
+
init_host = random.choice(self.hosts)
|
|
234
|
+
conn = await self._create_connection_impl(init_host)
|
|
235
|
+
|
|
236
|
+
# 记录本次创建的连接
|
|
237
|
+
conn_created_in_this_try = conn
|
|
206
238
|
|
|
207
|
-
# 步骤 B: 更新状态和初始化通道 (在锁内进行,保证原子性)
|
|
208
239
|
async with self._lock:
|
|
209
240
|
if self._is_shutdown:
|
|
210
|
-
# 如果在创建连接期间,外部调用了 close,则必须立即清理刚创建的连接
|
|
211
|
-
logger.warning("⚠️ [ABORT] 检测到关闭信号,放弃初始化并清理资源")
|
|
212
241
|
raise RuntimeError("客户端已关闭")
|
|
213
242
|
|
|
243
|
+
# 提交新资源
|
|
214
244
|
self._connection = conn
|
|
215
|
-
self._channel = await self.
|
|
245
|
+
self._channel = await self._connection.channel()
|
|
246
|
+
await self._channel.set_qos(prefetch_count=self.prefetch_count)
|
|
216
247
|
self._initialized = True
|
|
217
|
-
|
|
218
|
-
|
|
248
|
+
|
|
249
|
+
# 所有权转移成功,清空临时引用,防止 finally 重复关闭
|
|
250
|
+
conn_created_in_this_try = None
|
|
251
|
+
|
|
252
|
+
logger.info(f"🚀 [INIT_OK] 连接池初始化完成: {init_host}")
|
|
219
253
|
|
|
220
254
|
except Exception as e:
|
|
221
|
-
logger.error(f"💥 [
|
|
222
|
-
|
|
223
|
-
#
|
|
224
|
-
if conn_created_in_this_try:
|
|
225
|
-
logger.warning(
|
|
226
|
-
f"🧹 [LEAK_PREVENTION] 检测到初始化失败,正在显式关闭刚创建的连接: {id(conn_created_in_this_try)}")
|
|
255
|
+
logger.error(f"💥 [INIT_FAIL] 初始化异常: {str(e)}")
|
|
256
|
+
|
|
257
|
+
# 这里现在可以合法访问 conn_created_in_this_try
|
|
258
|
+
if conn_created_in_this_try is not None:
|
|
227
259
|
try:
|
|
228
260
|
await conn_created_in_this_try.close()
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
except Exception as close_err:
|
|
232
|
-
logger.error(f"❌ [CLOSE_ERR] 关闭泄漏连接时出错: {str(close_err)}")
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
233
263
|
|
|
234
|
-
# 如果是因为中途关闭导致的错误,不需要再次调用全局 close,否则调用
|
|
235
264
|
if not self._is_shutdown:
|
|
236
265
|
await self.close()
|
|
237
266
|
raise
|
|
238
267
|
|
|
268
|
+
async def force_reconnect(self):
|
|
269
|
+
"""
|
|
270
|
+
强制重连
|
|
271
|
+
严格执行:清理所有资源 -> 尝试建立新资源
|
|
272
|
+
"""
|
|
273
|
+
async with self._lock:
|
|
274
|
+
if self._is_shutdown:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
logger.warning("🔄 [FORCE_RECONNECT] 开始强制重连...")
|
|
278
|
+
|
|
279
|
+
# 1. 【关键】标记未初始化,迫使 _ensure_main_channel 走清理流程
|
|
280
|
+
self._initialized = False
|
|
281
|
+
|
|
282
|
+
# 2. 【关键】立即清理旧资源 (在锁内)
|
|
283
|
+
await self._cleanup_resources()
|
|
284
|
+
|
|
285
|
+
# 此时 self._connection 和 self._channel 均为 None
|
|
286
|
+
|
|
287
|
+
# 3. 锁外触发恢复 (避免阻塞锁太久)
|
|
288
|
+
try:
|
|
289
|
+
await self.acquire_channel()
|
|
290
|
+
logger.info("✅ [FORCE_RECONNECT_OK] 强制重连成功")
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"❌ [FORCE_RECONNECT_FAIL] 强制重连失败: {e}")
|
|
293
|
+
raise
|
|
294
|
+
|
|
239
295
|
async def acquire_channel(self) -> Tuple[RobustChannel, AbstractRobustConnection]:
|
|
240
296
|
"""获取主通道"""
|
|
241
297
|
if not self._initialized and not self._is_shutdown:
|
|
@@ -243,22 +299,20 @@ class RabbitMQConnectionPool:
|
|
|
243
299
|
return await self._ensure_main_channel(), self._connection
|
|
244
300
|
|
|
245
301
|
async def publish_message(self, routing_key: str, message_body: bytes, exchange_name: str = "", **kwargs):
|
|
246
|
-
"""发布消息"""
|
|
247
302
|
channel, _ = await self.acquire_channel()
|
|
248
303
|
try:
|
|
249
304
|
exchange = channel.default_exchange if not exchange_name else await channel.get_exchange(exchange_name)
|
|
250
305
|
message = Message(body=message_body, **kwargs)
|
|
251
306
|
await exchange.publish(message, routing_key=routing_key)
|
|
252
|
-
logger.debug(f"📤 [PUBLISH] 消息发布成功 - RK: {routing_key}")
|
|
253
307
|
except Exception as e:
|
|
254
308
|
logger.error(f"❌ [PUBLISH_FAIL] 发布失败: {str(e)}")
|
|
255
309
|
raise
|
|
256
310
|
|
|
257
311
|
async def consume_queue(self, queue_name: str, callback: Callable[[AbstractMessage], asyncio.Future], auto_ack: bool = False, **kwargs):
|
|
258
|
-
"""消费队列"""
|
|
259
312
|
if not self._initialized:
|
|
260
313
|
await self.init_pools()
|
|
261
314
|
|
|
315
|
+
# 检查是否已存在
|
|
262
316
|
async with self._lock:
|
|
263
317
|
if self._is_shutdown:
|
|
264
318
|
raise RuntimeError("客户端已关闭")
|
|
@@ -268,20 +322,26 @@ class RabbitMQConnectionPool:
|
|
|
268
322
|
if not self._connection or self._connection.is_closed:
|
|
269
323
|
raise RuntimeError("连接不可用,无法启动消费")
|
|
270
324
|
|
|
325
|
+
# 声明队列 (使用主通道)
|
|
271
326
|
await self.declare_queue(queue_name, **kwargs)
|
|
272
327
|
|
|
273
328
|
try:
|
|
274
|
-
#
|
|
275
|
-
conn = self.
|
|
329
|
+
# 获取最新连接
|
|
330
|
+
_, conn = await self.acquire_channel()
|
|
331
|
+
|
|
332
|
+
# 创建消费者通道
|
|
276
333
|
consumer_channel = await conn.channel()
|
|
277
334
|
await consumer_channel.set_qos(prefetch_count=self.prefetch_count)
|
|
278
|
-
logger.info(
|
|
279
|
-
f"✅ [CONSUMER_CHANNEL_OK] 消费者通道创建: {id(consumer_channel)}")
|
|
280
335
|
|
|
281
336
|
async with self._lock:
|
|
337
|
+
# 再次检查,防止并发创建
|
|
282
338
|
if self._is_shutdown:
|
|
283
339
|
await consumer_channel.close()
|
|
284
340
|
return
|
|
341
|
+
if queue_name in self._consumer_channels:
|
|
342
|
+
await consumer_channel.close() # 其他协程已经创建了
|
|
343
|
+
return
|
|
344
|
+
|
|
285
345
|
self._consumer_channels[queue_name] = consumer_channel
|
|
286
346
|
|
|
287
347
|
async def consume_callback_wrapper(message: AbstractMessage):
|
|
@@ -290,77 +350,45 @@ class RabbitMQConnectionPool:
|
|
|
290
350
|
if not auto_ack:
|
|
291
351
|
await message.ack()
|
|
292
352
|
except Exception as e:
|
|
293
|
-
logger.error(
|
|
294
|
-
f"❌ [CALLBACK_ERR] 消费回调异常 {queue_name}: {str(e)}")
|
|
353
|
+
logger.error(f"❌ [CALLBACK_ERR] {queue_name}: {e}")
|
|
295
354
|
if not auto_ack:
|
|
296
355
|
await message.nack(requeue=True)
|
|
297
356
|
|
|
298
357
|
await consumer_channel.basic_consume(
|
|
299
|
-
queue_name, consumer_callback=consume_callback_wrapper, auto_ack=auto_ack
|
|
358
|
+
queue_name, consumer_callback=consume_callback_wrapper, auto_ack=auto_ack
|
|
300
359
|
)
|
|
301
|
-
logger.info(f"🎧 [CONSUME_START]
|
|
360
|
+
logger.info(f"🎧 [CONSUME_START] {queue_name}")
|
|
302
361
|
|
|
303
362
|
except Exception as e:
|
|
304
|
-
logger.error(f"💥 [CONSUME_ERR]
|
|
363
|
+
logger.error(f"💥 [CONSUME_ERR] {queue_name}: {e}")
|
|
364
|
+
# 失败时清理字典
|
|
305
365
|
async with self._lock:
|
|
306
366
|
if queue_name in self._consumer_channels:
|
|
307
|
-
|
|
367
|
+
# 注意:这里清理的是字典里的引用,通道本身应该在 try 块里被关闭了吗?
|
|
368
|
+
# 如果 consumer_channel 创建成功但 basic_consume 失败,需要手动关闭
|
|
369
|
+
ch = self._consumer_channels.pop(queue_name, None)
|
|
370
|
+
if ch:
|
|
371
|
+
try:
|
|
372
|
+
await ch.close()
|
|
373
|
+
except:
|
|
374
|
+
pass
|
|
308
375
|
raise
|
|
309
376
|
|
|
310
377
|
async def close(self):
|
|
311
|
-
"""
|
|
312
|
-
资源销毁入口
|
|
313
|
-
"""
|
|
378
|
+
"""资源销毁"""
|
|
314
379
|
async with self._lock:
|
|
315
380
|
if self._is_shutdown:
|
|
316
381
|
return
|
|
317
382
|
self._is_shutdown = True
|
|
318
383
|
self._initialized = False
|
|
319
|
-
# 记录即将关闭的连接ID
|
|
320
|
-
conn_to_close_id = id(
|
|
321
|
-
self._connection) if self._connection else None
|
|
322
|
-
|
|
323
|
-
logger.info(f"🛑 [CLOSE_START] 开始关闭客户端... (准备关闭连接: {conn_to_close_id})")
|
|
324
384
|
|
|
325
|
-
|
|
326
|
-
channels_to_close = []
|
|
327
|
-
async with self._lock:
|
|
328
|
-
channels_to_close = list(self._consumer_channels.values())
|
|
329
|
-
self._consumer_channels.clear()
|
|
330
|
-
|
|
331
|
-
for ch in channels_to_close:
|
|
332
|
-
try:
|
|
333
|
-
if not ch.is_closed:
|
|
334
|
-
await ch.close()
|
|
335
|
-
logger.debug(f"✅ [CLOSE_CHANNEL] 消费者通道已关闭")
|
|
336
|
-
except Exception as e:
|
|
337
|
-
logger.warning(f"❌ [CLOSE_CHANNEL_ERR] 关闭消费者通道失败: {str(e)}")
|
|
385
|
+
logger.info("🛑 [CLOSE] 开始关闭连接池...")
|
|
338
386
|
|
|
339
|
-
#
|
|
340
|
-
|
|
341
|
-
try:
|
|
342
|
-
if not self._channel.is_closed:
|
|
343
|
-
await self._channel.close()
|
|
344
|
-
logger.info(f"✅ [CLOSE_CHANNEL] 主通道已关闭")
|
|
345
|
-
except Exception:
|
|
346
|
-
pass
|
|
347
|
-
self._channel = None
|
|
348
|
-
|
|
349
|
-
# 3. 关闭连接
|
|
350
|
-
# 确保在 finally 或显式 close 中调用 connection.close()
|
|
351
|
-
if self._connection:
|
|
352
|
-
try:
|
|
353
|
-
# 打印关闭操作
|
|
354
|
-
logger.info(f"🔌 [CLOSE_CONN] 正在关闭连接: {id(self._connection)}")
|
|
355
|
-
await self._connection.close()
|
|
356
|
-
logger.info(f"✅ [CLOSE_OK] 连接已成功关闭: {id(self._connection)}")
|
|
357
|
-
except Exception as e:
|
|
358
|
-
logger.warning(f"❌ [CLOSE_ERR] 关闭连接失败: {str(e)}")
|
|
359
|
-
self._connection = None
|
|
387
|
+
# 1. 清理所有资源
|
|
388
|
+
await self._cleanup_resources()
|
|
360
389
|
|
|
361
|
-
logger.info("🏁 [
|
|
390
|
+
logger.info("🏁 [CLOSE] 连接池已关闭")
|
|
362
391
|
|
|
363
|
-
# --- 辅助方法省略 (declare_queue 等) ---
|
|
364
392
|
async def declare_queue(self, queue_name: str, **kwargs) -> AbstractQueue:
|
|
365
393
|
channel, _ = await self.acquire_channel()
|
|
366
394
|
return await channel.declare_queue(queue_name, **kwargs)
|
|
@@ -70,8 +70,8 @@ class RabbitMQCoreService:
|
|
|
70
70
|
virtualhost=cls._config.get('virtual-host', "/"),
|
|
71
71
|
app_name=cls._config.get("APP_NAME", ""),
|
|
72
72
|
prefetch_count=global_prefetch_count,
|
|
73
|
-
heartbeat=cls._config.get('heartbeat',
|
|
74
|
-
connection_timeout=cls._config.get('connection_timeout',
|
|
73
|
+
heartbeat=cls._config.get('heartbeat', 15),
|
|
74
|
+
connection_timeout=cls._config.get('connection_timeout', 15),
|
|
75
75
|
reconnect_interval=cls._config.get('reconnect_interval', 5),
|
|
76
76
|
)
|
|
77
77
|
|
|
@@ -47,13 +47,13 @@ sycommon/models/mqsend_config.py,sha256=NQX9dc8PpuquMG36GCVhJe8omAW1KVXXqr6lSRU6
|
|
|
47
47
|
sycommon/models/sso_user.py,sha256=i1WAN6k5sPcPApQEdtjpWDy7VrzWLpOrOQewGLGoGIw,2702
|
|
48
48
|
sycommon/notice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
49
|
sycommon/notice/uvicorn_monitor.py,sha256=VryQYcAtjijJuGDBimbVurgwxlsLaLtkNnABPDY5Tao,7332
|
|
50
|
-
sycommon/rabbitmq/rabbitmq_client.py,sha256=
|
|
51
|
-
sycommon/rabbitmq/rabbitmq_pool.py,sha256=
|
|
50
|
+
sycommon/rabbitmq/rabbitmq_client.py,sha256=JZ73fc0Z8iMnayvRhRsnkEkBfzKF3wbxDKTE98RwVIA,17809
|
|
51
|
+
sycommon/rabbitmq/rabbitmq_pool.py,sha256=BiFQgZPzSAFR-n5XhyIafoeWQXETF_31nFRDhMbe6aU,15577
|
|
52
52
|
sycommon/rabbitmq/rabbitmq_service.py,sha256=XSHo9HuIJ_lq-vizRh4xJVdZr_2zLqeLhot09qb0euA,2025
|
|
53
53
|
sycommon/rabbitmq/rabbitmq_service_client_manager.py,sha256=MM4r8Pa2rjAmzy_NpHFb4thGznr6AYk6m__IC8IIxEM,7852
|
|
54
54
|
sycommon/rabbitmq/rabbitmq_service_connection_monitor.py,sha256=uvoMuJDzJ9i63uVRq1NKFV10CvkbGnTMyEoq2rgjQx8,3013
|
|
55
55
|
sycommon/rabbitmq/rabbitmq_service_consumer_manager.py,sha256=489r1RKd5WrTNMAcWCxUZpt9yWGrNunZlLCCp-M_rzM,11497
|
|
56
|
-
sycommon/rabbitmq/rabbitmq_service_core.py,sha256=
|
|
56
|
+
sycommon/rabbitmq/rabbitmq_service_core.py,sha256=6RMvIf78DmEOZmN8dA0duA9oy4ieNswdGrOeyJdD6tU,4753
|
|
57
57
|
sycommon/rabbitmq/rabbitmq_service_producer_manager.py,sha256=TJrLbvsjF55P9lwr7aCm9uRIRuC3z4EZNx74KEVKBtU,10190
|
|
58
58
|
sycommon/sentry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
59
|
sycommon/sentry/sy_sentry.py,sha256=e5Fbt9Gi2gIb048z0nuKbuhp3uCAEqYH2xXbF6qrZq4,1471
|
|
@@ -78,8 +78,8 @@ sycommon/tools/env.py,sha256=Ah-tBwG2C0_hwLGFebVQgKdWWXCjTzBuF23gCkLHYy4,2437
|
|
|
78
78
|
sycommon/tools/merge_headers.py,sha256=HV_i52Q-9se3SP8qh7ZGYl8bP7Fxtal4CGVkyMwEdM8,4373
|
|
79
79
|
sycommon/tools/snowflake.py,sha256=lVEe5mNCOgz5OqGQpf5_nXaGnRJlI2STX2s-ppTtanA,11947
|
|
80
80
|
sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
|
|
81
|
-
sycommon_python_lib-0.1.
|
|
82
|
-
sycommon_python_lib-0.1.
|
|
83
|
-
sycommon_python_lib-0.1.
|
|
84
|
-
sycommon_python_lib-0.1.
|
|
85
|
-
sycommon_python_lib-0.1.
|
|
81
|
+
sycommon_python_lib-0.1.56b12.dist-info/METADATA,sha256=dkuadRkMxJOl31ynqjcEezKtvoQ7On9qeGyGSnxCUZ0,7270
|
|
82
|
+
sycommon_python_lib-0.1.56b12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
83
|
+
sycommon_python_lib-0.1.56b12.dist-info/entry_points.txt,sha256=q_h2nbvhhmdnsOUZEIwpuoDjaNfBF9XqppDEmQn9d_A,46
|
|
84
|
+
sycommon_python_lib-0.1.56b12.dist-info/top_level.txt,sha256=98CJ-cyM2WIKxLz-Pf0AitWLhJyrfXvyY8slwjTXNuc,17
|
|
85
|
+
sycommon_python_lib-0.1.56b12.dist-info/RECORD,,
|
|
File without changes
|
{sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.56b11.dist-info → sycommon_python_lib-0.1.56b12.dist-info}/top_level.txt
RENAMED
|
File without changes
|