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