sycommon-python-lib 0.1.56b5__py3-none-any.whl → 0.1.57b4__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/config/Config.py +24 -3
- sycommon/config/LangfuseConfig.py +15 -0
- sycommon/config/SentryConfig.py +13 -0
- sycommon/llm/embedding.py +269 -50
- sycommon/llm/get_llm.py +9 -218
- sycommon/llm/struct_token.py +192 -0
- sycommon/llm/sy_langfuse.py +103 -0
- sycommon/llm/usage_token.py +117 -0
- sycommon/logging/kafka_log.py +187 -433
- sycommon/middleware/exception.py +10 -16
- sycommon/middleware/timeout.py +2 -1
- sycommon/middleware/traceid.py +81 -76
- sycommon/notice/uvicorn_monitor.py +32 -27
- sycommon/rabbitmq/rabbitmq_client.py +247 -242
- sycommon/rabbitmq/rabbitmq_pool.py +201 -123
- sycommon/rabbitmq/rabbitmq_service.py +25 -843
- sycommon/rabbitmq/rabbitmq_service_client_manager.py +211 -0
- sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +73 -0
- sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +285 -0
- sycommon/rabbitmq/rabbitmq_service_core.py +117 -0
- sycommon/rabbitmq/rabbitmq_service_producer_manager.py +238 -0
- sycommon/sentry/__init__.py +0 -0
- sycommon/sentry/sy_sentry.py +35 -0
- sycommon/services.py +122 -96
- sycommon/synacos/nacos_client_base.py +121 -0
- sycommon/synacos/nacos_config_manager.py +107 -0
- sycommon/synacos/nacos_heartbeat_manager.py +144 -0
- sycommon/synacos/nacos_service.py +63 -783
- sycommon/synacos/nacos_service_discovery.py +157 -0
- sycommon/synacos/nacos_service_registration.py +270 -0
- sycommon/tools/env.py +62 -0
- sycommon/tools/merge_headers.py +20 -0
- sycommon/tools/snowflake.py +101 -153
- {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/METADATA +10 -8
- {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/RECORD +38 -20
- {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.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__(
|
|
@@ -35,6 +27,7 @@ class RabbitMQClient:
|
|
|
35
27
|
exchange_name: str = "system.topic.exchange",
|
|
36
28
|
exchange_type: str = "topic",
|
|
37
29
|
queue_name: Optional[str] = None,
|
|
30
|
+
app_name: Optional[str] = None,
|
|
38
31
|
routing_key: str = "#",
|
|
39
32
|
durable: bool = True,
|
|
40
33
|
auto_delete: bool = False,
|
|
@@ -42,12 +35,10 @@ class RabbitMQClient:
|
|
|
42
35
|
create_if_not_exists: bool = True,
|
|
43
36
|
**kwargs,
|
|
44
37
|
):
|
|
45
|
-
# 依赖注入:连接池(必须已初始化)
|
|
46
38
|
self.connection_pool = connection_pool
|
|
47
39
|
if not self.connection_pool._initialized:
|
|
48
40
|
raise RuntimeError("连接池未初始化,请先调用 connection_pool.init_pools()")
|
|
49
41
|
|
|
50
|
-
# 交换机配置
|
|
51
42
|
self.exchange_name = exchange_name.strip()
|
|
52
43
|
try:
|
|
53
44
|
self.exchange_type = ExchangeType(exchange_type.lower())
|
|
@@ -55,17 +46,17 @@ class RabbitMQClient:
|
|
|
55
46
|
logger.warning(f"无效的exchange_type: {exchange_type},默认使用'topic'")
|
|
56
47
|
self.exchange_type = ExchangeType.TOPIC
|
|
57
48
|
|
|
58
|
-
|
|
49
|
+
self.app_name = app_name.strip() if app_name else None
|
|
59
50
|
self.queue_name = queue_name.strip() if queue_name else None
|
|
60
51
|
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
|
|
52
|
+
self.durable = durable
|
|
53
|
+
self.auto_delete = auto_delete
|
|
54
|
+
self.auto_parse_json = auto_parse_json
|
|
55
|
+
self.create_if_not_exists = create_if_not_exists
|
|
65
56
|
|
|
66
|
-
#
|
|
57
|
+
# 资源状态
|
|
67
58
|
self._channel: Optional[Channel] = None
|
|
68
|
-
self._channel_conn: Optional[AbstractRobustConnection] = None
|
|
59
|
+
self._channel_conn: Optional[AbstractRobustConnection] = None
|
|
69
60
|
self._exchange: Optional[AbstractExchange] = None
|
|
70
61
|
self._queue: Optional[AbstractQueue] = None
|
|
71
62
|
self._consumer_tag: Optional[ConsumerTag] = None
|
|
@@ -73,45 +64,38 @@ class RabbitMQClient:
|
|
|
73
64
|
MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]]] = None
|
|
74
65
|
self._closed = False
|
|
75
66
|
|
|
76
|
-
#
|
|
67
|
+
# 并发控制
|
|
77
68
|
self._consume_lock = asyncio.Lock()
|
|
78
69
|
self._connect_lock = asyncio.Lock()
|
|
79
|
-
|
|
70
|
+
|
|
71
|
+
# 防止并发重连覆盖
|
|
72
|
+
self._connecting = False
|
|
73
|
+
self._connect_condition = asyncio.Condition()
|
|
74
|
+
|
|
80
75
|
self._conn_close_callback: Optional[Callable] = None
|
|
81
|
-
# 控制重连频率的信号量(限制并发重连数)
|
|
82
76
|
self._reconnect_semaphore = asyncio.Semaphore(1)
|
|
83
|
-
# 固定重连间隔15秒(全局统一)
|
|
84
|
-
self._RECONNECT_INTERVAL = 15
|
|
85
|
-
# 跟踪当前重连任务(避免重复创建)
|
|
86
77
|
self._current_reconnect_task: Optional[asyncio.Task] = None
|
|
87
|
-
|
|
88
|
-
self._reconnect_fail_count = 0
|
|
89
|
-
# 连接失败告警阈值
|
|
90
|
-
self._reconnect_alert_threshold = 5
|
|
78
|
+
self._RECONNECT_INTERVAL = 15
|
|
91
79
|
|
|
92
80
|
@property
|
|
93
81
|
async def is_connected(self) -> bool:
|
|
94
|
-
"""异步检查客户端连接状态(属性,不可调用)"""
|
|
95
82
|
if self._closed:
|
|
96
83
|
return False
|
|
97
84
|
try:
|
|
98
|
-
# 单通道场景:校验通道+连接+核心资源都有效
|
|
99
85
|
return (
|
|
100
86
|
self._channel and not self._channel.is_closed
|
|
101
87
|
and self._channel_conn and not self._channel_conn.is_closed
|
|
102
88
|
and self._exchange is not None
|
|
103
89
|
and (not self.queue_name or self._queue is not None)
|
|
104
90
|
)
|
|
105
|
-
except Exception
|
|
106
|
-
logger.warning(f"检查连接状态失败: {str(e)}")
|
|
91
|
+
except Exception:
|
|
107
92
|
return False
|
|
108
93
|
|
|
109
94
|
async def _rebuild_resources(self) -> None:
|
|
110
|
-
"""重建交换机/队列等资源(依赖已有的通道)"""
|
|
111
95
|
if not self._channel or self._channel.is_closed:
|
|
112
96
|
raise RuntimeError("无有效通道,无法重建资源")
|
|
113
97
|
|
|
114
|
-
#
|
|
98
|
+
# 声明交换机
|
|
115
99
|
self._exchange = await self._channel.declare_exchange(
|
|
116
100
|
name=self.exchange_name,
|
|
117
101
|
type=self.exchange_type,
|
|
@@ -119,111 +103,144 @@ class RabbitMQClient:
|
|
|
119
103
|
auto_delete=self.auto_delete,
|
|
120
104
|
passive=not self.create_if_not_exists,
|
|
121
105
|
)
|
|
122
|
-
logger.info(
|
|
123
|
-
f"交换机重建成功: {self.exchange_name}(类型: {self.exchange_type.value})")
|
|
106
|
+
logger.info(f"交换机重建成功: {self.exchange_name}")
|
|
124
107
|
|
|
125
|
-
#
|
|
126
|
-
if self.queue_name:
|
|
108
|
+
# 声明队列
|
|
109
|
+
if self.queue_name and self.queue_name.endswith(f".{self.app_name}"):
|
|
127
110
|
self._queue = await self._channel.declare_queue(
|
|
128
111
|
name=self.queue_name,
|
|
129
112
|
durable=self.durable,
|
|
130
113
|
auto_delete=self.auto_delete,
|
|
131
114
|
passive=not self.create_if_not_exists,
|
|
132
115
|
)
|
|
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
|
-
)
|
|
116
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
117
|
+
logger.info(f"队列重建成功: {self.queue_name}")
|
|
142
118
|
|
|
143
119
|
async def connect(self) -> None:
|
|
144
120
|
if self._closed:
|
|
145
121
|
raise RuntimeError("客户端已关闭,无法重新连接")
|
|
146
122
|
|
|
123
|
+
# 1. 并发控制:使用 _connect_lock 保证只有一个协程在执行连接流程
|
|
147
124
|
async with self._connect_lock:
|
|
148
|
-
#
|
|
149
|
-
if self.
|
|
125
|
+
# 如果已经在连了,等待其完成
|
|
126
|
+
if self._connecting:
|
|
127
|
+
logger.debug("连接正在进行中,等待现有连接完成...")
|
|
150
128
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
129
|
+
# 等待条件变量,超时设为 60 秒防止死等
|
|
130
|
+
await asyncio.wait_for(
|
|
131
|
+
self._connect_condition.wait_for(
|
|
132
|
+
lambda: not self._connecting),
|
|
133
|
+
timeout=60.0
|
|
134
|
+
)
|
|
135
|
+
except asyncio.TimeoutError:
|
|
136
|
+
raise RuntimeError("等待连接超时")
|
|
155
137
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
self._queue = None
|
|
161
|
-
self._conn_close_callback = None
|
|
138
|
+
# 等待结束后,再次检查状态
|
|
139
|
+
if not await self.is_connected:
|
|
140
|
+
raise RuntimeError("等待重连后,连接状态依然无效")
|
|
141
|
+
return
|
|
162
142
|
|
|
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)
|
|
143
|
+
# 标记开始连接
|
|
144
|
+
self._connecting = True
|
|
179
145
|
|
|
180
|
-
|
|
181
|
-
|
|
146
|
+
# 释放 _connect_lock,允许其他协程读取状态,但在连接完成前阻止新的连接请求
|
|
147
|
+
# 注意:这里释放了 _connect_lock,但 self._connecting = True 阻止了新的连接流程
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# --- 阶段1: 清理旧资源 ---
|
|
151
|
+
# 重新获取锁进行资源清理
|
|
152
|
+
async with self._connect_lock:
|
|
153
|
+
was_consuming = self._consumer_tag is not None
|
|
182
154
|
|
|
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
155
|
if self._channel_conn and self._conn_close_callback:
|
|
190
156
|
try:
|
|
191
157
|
self._channel_conn.close_callbacks.discard(
|
|
192
158
|
self._conn_close_callback)
|
|
193
159
|
except Exception:
|
|
194
160
|
pass
|
|
195
|
-
|
|
161
|
+
|
|
196
162
|
self._channel = None
|
|
197
163
|
self._channel_conn = None
|
|
164
|
+
self._exchange = None
|
|
165
|
+
self._queue = None
|
|
198
166
|
self._conn_close_callback = None
|
|
199
167
|
|
|
200
|
-
|
|
201
|
-
|
|
168
|
+
# --- 阶段2: 获取新连接 (耗时IO) ---
|
|
169
|
+
self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
|
|
170
|
+
|
|
171
|
+
# 设置回调
|
|
172
|
+
def on_conn_closed(conn, exc):
|
|
173
|
+
logger.warning(f"检测到连接关闭: {exc}")
|
|
174
|
+
if not self._closed and not self._connecting:
|
|
202
175
|
asyncio.create_task(self._safe_reconnect())
|
|
203
|
-
|
|
176
|
+
|
|
177
|
+
self._conn_close_callback = on_conn_closed
|
|
178
|
+
if self._channel_conn:
|
|
179
|
+
self._channel_conn.close_callbacks.add(
|
|
180
|
+
self._conn_close_callback)
|
|
181
|
+
|
|
182
|
+
# 重建资源
|
|
183
|
+
await self._rebuild_resources()
|
|
184
|
+
|
|
185
|
+
# --- 阶段3: 恢复消费 ---
|
|
186
|
+
if was_consuming and self._message_handler and self.queue_name and self.queue_name.endswith(f".{self.app_name}"):
|
|
187
|
+
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复...")
|
|
188
|
+
try:
|
|
189
|
+
self._queue = await self._channel.declare_queue(
|
|
190
|
+
name=self.queue_name,
|
|
191
|
+
durable=self.durable,
|
|
192
|
+
auto_delete=self.auto_delete,
|
|
193
|
+
passive=False,
|
|
194
|
+
)
|
|
195
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
196
|
+
self._consumer_tag = await self._queue.consume(self._process_message_callback)
|
|
197
|
+
logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
200
|
+
self._consumer_tag = None
|
|
201
|
+
else:
|
|
202
|
+
self._consumer_tag = None
|
|
203
|
+
|
|
204
|
+
logger.info("客户端连接初始化完成")
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
208
|
+
|
|
209
|
+
# 异常时清理资源
|
|
210
|
+
async with self._connect_lock:
|
|
211
|
+
if self._channel_conn and self._conn_close_callback:
|
|
212
|
+
self._channel_conn.close_callbacks.discard(
|
|
213
|
+
self._conn_close_callback)
|
|
214
|
+
self._channel = None
|
|
215
|
+
self._channel_conn = None
|
|
216
|
+
self._consumer_tag = None
|
|
217
|
+
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
finally:
|
|
221
|
+
# 【关键修复】必须在持有 Condition 内部锁的情况下调用 notify_all
|
|
222
|
+
# 这里使用 async with self._connect_condition: 自动完成 acquire() ... notify_all() ... release()
|
|
223
|
+
async with self._connect_condition:
|
|
224
|
+
self._connecting = False
|
|
225
|
+
self._connect_condition.notify_all()
|
|
204
226
|
|
|
205
227
|
async def _safe_reconnect(self):
|
|
206
|
-
"""
|
|
228
|
+
"""安全重连任务(仅用于被动监听连接关闭)"""
|
|
207
229
|
async with self._reconnect_semaphore:
|
|
208
|
-
|
|
209
|
-
if self._current_reconnect_task and not self._current_reconnect_task.done():
|
|
210
|
-
logger.debug("已有重连任务在运行,跳过重复触发")
|
|
230
|
+
if self._closed:
|
|
211
231
|
return
|
|
212
232
|
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
# 如果已经在重连,直接忽略
|
|
234
|
+
if self._connecting:
|
|
215
235
|
return
|
|
216
236
|
|
|
217
|
-
# 固定15秒重连间隔
|
|
218
237
|
logger.info(f"将在{self._RECONNECT_INTERVAL}秒后尝试重连...")
|
|
219
238
|
await asyncio.sleep(self._RECONNECT_INTERVAL)
|
|
220
239
|
|
|
221
240
|
if self._closed or await self.is_connected:
|
|
222
|
-
logger.debug("重连等待期间客户端状态变化,取消重连")
|
|
223
241
|
return
|
|
224
242
|
|
|
225
243
|
try:
|
|
226
|
-
logger.info("开始重连RabbitMQ客户端...")
|
|
227
244
|
self._current_reconnect_task = asyncio.create_task(
|
|
228
245
|
self.connect())
|
|
229
246
|
await self._current_reconnect_task
|
|
@@ -232,113 +249,131 @@ class RabbitMQClient:
|
|
|
232
249
|
finally:
|
|
233
250
|
self._current_reconnect_task = None
|
|
234
251
|
|
|
235
|
-
async def set_message_handler(
|
|
236
|
-
self,
|
|
237
|
-
handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]],
|
|
238
|
-
) -> None:
|
|
239
|
-
"""设置消息处理器(必须是协程函数)"""
|
|
252
|
+
async def set_message_handler(self, handler: Callable[..., Coroutine]) -> None:
|
|
240
253
|
if not asyncio.iscoroutinefunction(handler):
|
|
241
|
-
raise TypeError("
|
|
242
|
-
|
|
254
|
+
raise TypeError("消息处理器必须是协程函数")
|
|
243
255
|
async with self._consume_lock:
|
|
244
256
|
self._message_handler = handler
|
|
245
|
-
|
|
257
|
+
|
|
258
|
+
async def _process_message_callback(self, message: AbstractIncomingMessage):
|
|
259
|
+
# 定义标志位,记录我们是否需要重试(即业务是否失败)
|
|
260
|
+
should_retry = False
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
msg_obj: MQMsgModel
|
|
264
|
+
if self.auto_parse_json:
|
|
265
|
+
try:
|
|
266
|
+
body_dict = json.loads(message.body.decode("utf-8"))
|
|
267
|
+
msg_obj = MQMsgModel(**body_dict)
|
|
268
|
+
except json.JSONDecodeError as e:
|
|
269
|
+
logger.error(f"JSON解析失败: {e}")
|
|
270
|
+
# 格式错误,无法处理,直接拒绝不重试
|
|
271
|
+
await message.reject(requeue=False)
|
|
272
|
+
return # 这里 return 了,不会走下面的 finally
|
|
273
|
+
else:
|
|
274
|
+
msg_obj = MQMsgModel(
|
|
275
|
+
body=message.body.decode("utf-8"),
|
|
276
|
+
routing_key=message.routing_key,
|
|
277
|
+
delivery_tag=message.delivery_tag,
|
|
278
|
+
traceId=message.headers.get("trace-id"),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
SYLogger.set_trace_id(msg_obj.traceId)
|
|
282
|
+
|
|
283
|
+
if self._message_handler:
|
|
284
|
+
await self._message_handler(msg_obj, message)
|
|
285
|
+
|
|
286
|
+
# 如果正常执行到这里,说明业务成功
|
|
287
|
+
# await message.ack()
|
|
288
|
+
# 我们移除这里的 ack,统一交给 finally 处理
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"消息处理异常: {e}", exc_info=True)
|
|
292
|
+
# 业务异常,标记需要重试
|
|
293
|
+
should_retry = True
|
|
294
|
+
|
|
295
|
+
finally:
|
|
296
|
+
# 【核心修复】无论发生什么,最后都要给 MQ 一个交待
|
|
297
|
+
if should_retry:
|
|
298
|
+
headers = dict(message.headers) if message.headers else {}
|
|
299
|
+
current_retry = int(headers.get("x-retry-count", 0))
|
|
300
|
+
|
|
301
|
+
if current_retry >= 3:
|
|
302
|
+
logger.warning(f"重试次数超限,丢弃消息: {message.delivery_tag}")
|
|
303
|
+
await message.reject(requeue=False)
|
|
304
|
+
else:
|
|
305
|
+
headers["x-retry-count"] = current_retry + 1
|
|
306
|
+
try:
|
|
307
|
+
new_msg = Message(
|
|
308
|
+
body=message.body,
|
|
309
|
+
headers=headers,
|
|
310
|
+
content_type=message.content_type,
|
|
311
|
+
delivery_mode=message.delivery_mode
|
|
312
|
+
)
|
|
313
|
+
# 发送新消息用于重试
|
|
314
|
+
await self._exchange.publish(new_msg, routing_key=message.routing_key)
|
|
315
|
+
|
|
316
|
+
# 【关键】新消息发成功了,现在可以安全地 Ack 掉旧消息了
|
|
317
|
+
# 这样旧消息才会从队列中移除,避免死循环
|
|
318
|
+
await message.ack()
|
|
319
|
+
|
|
320
|
+
except Exception as pub_err:
|
|
321
|
+
logger.error(f"重试发布失败,消息将丢失: {pub_err}")
|
|
322
|
+
# 发布失败,无法重试,只能丢弃旧消息(或者 Nack requeue=True)
|
|
323
|
+
# 为了防止死循环,这里通常建议 Reject (False) 并配置死信队列
|
|
324
|
+
await message.reject(requeue=False)
|
|
325
|
+
else:
|
|
326
|
+
# 业务正常执行,直接 Ack
|
|
327
|
+
await message.ack()
|
|
246
328
|
|
|
247
329
|
async def start_consuming(self) -> Optional[ConsumerTag]:
|
|
248
|
-
"""启动消息消费(支持自动重连)"""
|
|
249
330
|
if self._closed:
|
|
250
331
|
raise RuntimeError("客户端已关闭,无法启动消费")
|
|
251
332
|
|
|
252
333
|
async with self._consume_lock:
|
|
253
|
-
# 1. 校验前置条件
|
|
254
334
|
if not self._message_handler:
|
|
255
|
-
raise RuntimeError("
|
|
335
|
+
raise RuntimeError("未设置消息处理器")
|
|
336
|
+
|
|
256
337
|
if not await self.is_connected:
|
|
257
338
|
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
|
-
if self.auto_parse_json:
|
|
266
|
-
try:
|
|
267
|
-
body_dict = json.loads(
|
|
268
|
-
message.body.decode("utf-8"))
|
|
269
|
-
msg_obj = MQMsgModel(**body_dict)
|
|
270
|
-
except json.JSONDecodeError as e:
|
|
271
|
-
logger.error(
|
|
272
|
-
f"JSON消息解析失败: {str(e)},消息体: {message.body[:100]}...")
|
|
273
|
-
await message.nack(requeue=False) # 解析失败,不重入队
|
|
274
|
-
return
|
|
275
|
-
else:
|
|
276
|
-
msg_obj = MQMsgModel(
|
|
277
|
-
body=message.body.decode("utf-8"),
|
|
278
|
-
routing_key=message.routing_key,
|
|
279
|
-
delivery_tag=message.delivery_tag,
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# 调用消息处理器
|
|
283
|
-
await self._message_handler(msg_obj, message)
|
|
284
339
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
f"消息处理失败,delivery_tag: {message.delivery_tag}",
|
|
293
|
-
exc_info=True
|
|
340
|
+
if not self._queue:
|
|
341
|
+
if self.queue_name and self.queue_name.endswith(f".{self.app_name}"):
|
|
342
|
+
self._queue = await self._channel.declare_queue(
|
|
343
|
+
name=self.queue_name,
|
|
344
|
+
durable=self.durable,
|
|
345
|
+
auto_delete=self.auto_delete,
|
|
346
|
+
passive=not self.create_if_not_exists,
|
|
294
347
|
)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
f"消息已重入队过,本次拒绝入队: {message.delivery_tag}")
|
|
299
|
-
await message.reject(requeue=False)
|
|
300
|
-
else:
|
|
301
|
-
logger.warning(f"消息重入队: {message.delivery_tag}")
|
|
302
|
-
await message.nack(requeue=True)
|
|
303
|
-
|
|
304
|
-
# 连接失效则触发重连
|
|
305
|
-
if not await self.is_connected:
|
|
306
|
-
logger.warning("连接已失效,触发客户端重连")
|
|
307
|
-
asyncio.create_task(self._safe_reconnect())
|
|
348
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
349
|
+
else:
|
|
350
|
+
raise RuntimeError("未配置队列名")
|
|
308
351
|
|
|
309
|
-
|
|
310
|
-
self._consumer_tag = await self._queue.consume(consume_callback)
|
|
352
|
+
self._consumer_tag = await self._queue.consume(self._process_message_callback)
|
|
311
353
|
logger.info(
|
|
312
|
-
f"开始消费队列: {self._queue.name},
|
|
313
|
-
)
|
|
354
|
+
f"开始消费队列: {self._queue.name},tag: {self._consumer_tag}")
|
|
314
355
|
return self._consumer_tag
|
|
315
356
|
|
|
316
357
|
async def stop_consuming(self) -> None:
|
|
317
|
-
"""停止消息消费(适配 RobustChannel)"""
|
|
318
358
|
async with self._consume_lock:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if self._consumer_tag and self._queue and self._channel and not self._channel.is_closed:
|
|
322
|
-
# 使用队列的 cancel 方法(适配 RobustChannel)
|
|
359
|
+
if self._consumer_tag and self._queue and self._channel:
|
|
360
|
+
try:
|
|
323
361
|
await self._queue.cancel(self._consumer_tag)
|
|
324
|
-
logger.info(
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
340
|
-
finally:
|
|
341
|
-
self._consumer_tag = None # 无论成败,清理消费标签
|
|
362
|
+
logger.info(f"停止消费成功: {self._consumer_tag}")
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.warning(f"停止消费异常: {e}")
|
|
365
|
+
self._consumer_tag = None
|
|
366
|
+
|
|
367
|
+
async def _handle_publish_failure(self):
|
|
368
|
+
try:
|
|
369
|
+
logger.info("检测到发布异常,强制连接池切换节点...")
|
|
370
|
+
await self.connection_pool.force_reconnect()
|
|
371
|
+
# 连接池切换后,必须刷新客户端资源
|
|
372
|
+
await self.connect()
|
|
373
|
+
logger.info("故障转移完成,资源已刷新")
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"故障转移失败: {e}")
|
|
376
|
+
raise
|
|
342
377
|
|
|
343
378
|
async def publish(
|
|
344
379
|
self,
|
|
@@ -348,18 +383,9 @@ class RabbitMQClient:
|
|
|
348
383
|
delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT,
|
|
349
384
|
retry_count: int = 3,
|
|
350
385
|
) -> None:
|
|
351
|
-
"""
|
|
352
|
-
发布消息(支持自动重试、mandatory路由校验、5秒超时控制)
|
|
353
|
-
:param message_body: 消息体(字符串/字典/MQMsgModel)
|
|
354
|
-
:param headers: 消息头(可选)
|
|
355
|
-
:param content_type: 内容类型(默认application/json)
|
|
356
|
-
:param delivery_mode: 投递模式(PERSISTENT=持久化,TRANSIENT=非持久化)
|
|
357
|
-
:param retry_count: 重试次数(默认3次)
|
|
358
|
-
"""
|
|
359
386
|
if self._closed:
|
|
360
387
|
raise RuntimeError("客户端已关闭,无法发布消息")
|
|
361
388
|
|
|
362
|
-
# 处理消息体序列化
|
|
363
389
|
try:
|
|
364
390
|
if isinstance(message_body, MQMsgModel):
|
|
365
391
|
body = json.dumps(message_body.to_dict(),
|
|
@@ -372,93 +398,72 @@ class RabbitMQClient:
|
|
|
372
398
|
else:
|
|
373
399
|
raise TypeError(f"不支持的消息体类型: {type(message_body)}")
|
|
374
400
|
except Exception as e:
|
|
375
|
-
logger.error(f"消息体序列化失败: {
|
|
401
|
+
logger.error(f"消息体序列化失败: {e}")
|
|
376
402
|
raise
|
|
377
403
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
headers=headers or {},
|
|
382
|
-
content_type=content_type,
|
|
383
|
-
delivery_mode=delivery_mode,
|
|
384
|
-
)
|
|
404
|
+
message = Message(body=body, headers=headers or {},
|
|
405
|
+
content_type=content_type, delivery_mode=delivery_mode)
|
|
406
|
+
last_exception = None
|
|
385
407
|
|
|
386
|
-
# 发布重试逻辑
|
|
387
408
|
for retry in range(retry_count):
|
|
388
409
|
try:
|
|
389
|
-
# 确保连接有效
|
|
390
410
|
if not await self.is_connected:
|
|
391
|
-
logger.warning(f"发布消息前连接失效,触发重连(retry: {retry})")
|
|
392
411
|
await self.connect()
|
|
393
412
|
|
|
394
|
-
|
|
395
|
-
publish_result = await self._exchange.publish(
|
|
413
|
+
result = await self._exchange.publish(
|
|
396
414
|
message=message,
|
|
397
|
-
routing_key=self.routing_key
|
|
415
|
+
routing_key=self.routing_key,
|
|
398
416
|
mandatory=True,
|
|
399
417
|
timeout=5.0
|
|
400
418
|
)
|
|
401
419
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
raise RuntimeError(
|
|
405
|
-
f"消息未找到匹配的队列(routing_key: {self.routing_key}),mandatory=True 触发失败"
|
|
406
|
-
)
|
|
420
|
+
if result is None:
|
|
421
|
+
raise RuntimeError(f"消息未找到匹配的队列: {self.routing_key}")
|
|
407
422
|
|
|
408
|
-
logger.info(
|
|
409
|
-
f"消息发布成功(retry: {retry}),routing_key: {self.routing_key},"
|
|
410
|
-
f"delivery_mode: {delivery_mode.value},mandatory: True,timeout: 5.0s"
|
|
411
|
-
)
|
|
423
|
+
logger.info(f"发布成功: {self.routing_key}")
|
|
412
424
|
return
|
|
413
|
-
|
|
414
|
-
logger.error(
|
|
415
|
-
f"消息发布超时(retry: {retry}/{retry_count-1}),超时时间: 5.0s"
|
|
416
|
-
)
|
|
425
|
+
|
|
417
426
|
except RuntimeError as e:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
)
|
|
427
|
+
if "未找到匹配的队列" in str(e):
|
|
428
|
+
raise
|
|
429
|
+
last_exception = str(e)
|
|
430
|
+
await self._handle_publish_failure()
|
|
431
|
+
|
|
421
432
|
except Exception as e:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
426
|
-
# 清理失效状态,下次重试重连
|
|
427
|
-
self._exchange = None
|
|
428
|
-
# 指数退避重试间隔
|
|
429
|
-
await asyncio.sleep(0.5 * (2 ** retry))
|
|
433
|
+
last_exception = str(e)
|
|
434
|
+
logger.error(f"发布异常: {e}")
|
|
435
|
+
await self._handle_publish_failure()
|
|
430
436
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
f"mandatory: True,timeout: 5.0s"
|
|
435
|
-
)
|
|
437
|
+
await asyncio.sleep(5)
|
|
438
|
+
|
|
439
|
+
raise RuntimeError(f"消息发布最终失败: {last_exception}")
|
|
436
440
|
|
|
437
441
|
async def close(self) -> None:
|
|
438
|
-
"""关闭客户端(移除回调+释放资源)"""
|
|
439
442
|
self._closed = True
|
|
440
443
|
logger.info("开始关闭RabbitMQ客户端...")
|
|
441
444
|
|
|
442
|
-
# 停止重连任务
|
|
443
445
|
if self._current_reconnect_task and not self._current_reconnect_task.done():
|
|
444
446
|
self._current_reconnect_task.cancel()
|
|
445
447
|
try:
|
|
446
448
|
await self._current_reconnect_task
|
|
447
449
|
except asyncio.CancelledError:
|
|
448
|
-
|
|
450
|
+
pass
|
|
449
451
|
|
|
450
|
-
# 1. 停止消费
|
|
451
452
|
await self.stop_consuming()
|
|
452
453
|
|
|
453
|
-
# 2. 清理回调+状态(单通道无需归还,连接池统一管理)
|
|
454
454
|
async with self._connect_lock:
|
|
455
455
|
if self._conn_close_callback and self._channel_conn:
|
|
456
456
|
self._channel_conn.close_callbacks.discard(
|
|
457
457
|
self._conn_close_callback)
|
|
458
|
+
|
|
458
459
|
self._channel = None
|
|
459
460
|
self._channel_conn = None
|
|
460
461
|
self._exchange = None
|
|
461
462
|
self._queue = None
|
|
462
463
|
self._message_handler = None
|
|
463
464
|
|
|
464
|
-
|
|
465
|
+
# 确保唤醒可能正在等待 connect 的任务
|
|
466
|
+
self._connecting = False
|
|
467
|
+
self._connect_condition.notify_all()
|
|
468
|
+
|
|
469
|
+
logger.info("客户端已关闭")
|