sycommon-python-lib 0.1.40__tar.gz → 0.1.42__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/PKG-INFO +1 -1
  2. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/pyproject.toml +1 -1
  3. sycommon_python_lib-0.1.42/src/sycommon/rabbitmq/rabbitmq_client.py +379 -0
  4. sycommon_python_lib-0.1.42/src/sycommon/rabbitmq/rabbitmq_pool.py +330 -0
  5. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/rabbitmq/rabbitmq_service.py +223 -144
  6. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
  7. sycommon_python_lib-0.1.40/src/sycommon/rabbitmq/rabbitmq_client.py +0 -300
  8. sycommon_python_lib-0.1.40/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -180
  9. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/README.md +0 -0
  10. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/setup.cfg +0 -0
  11. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/command/cli.py +0 -0
  12. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/__init__.py +0 -0
  13. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/Config.py +0 -0
  14. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/DatabaseConfig.py +0 -0
  15. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/EmbeddingConfig.py +0 -0
  16. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/LLMConfig.py +0 -0
  17. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/MQConfig.py +0 -0
  18. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/RerankerConfig.py +0 -0
  19. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/config/__init__.py +0 -0
  20. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/database/base_db_service.py +0 -0
  21. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/database/database_service.py +0 -0
  22. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/health/__init__.py +0 -0
  23. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/health/health_check.py +0 -0
  24. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/health/metrics.py +0 -0
  25. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/health/ping.py +0 -0
  26. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/logging/__init__.py +0 -0
  27. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/logging/kafka_log.py +0 -0
  28. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/logging/logger_wrapper.py +0 -0
  29. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/logging/sql_logger.py +0 -0
  30. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/__init__.py +0 -0
  31. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/context.py +0 -0
  32. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/cors.py +0 -0
  33. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/docs.py +0 -0
  34. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/exception.py +0 -0
  35. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/middleware.py +0 -0
  36. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/monitor_memory.py +0 -0
  37. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/mq.py +0 -0
  38. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/timeout.py +0 -0
  39. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/middleware/traceid.py +0 -0
  40. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/__init__.py +0 -0
  41. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/base_http.py +0 -0
  42. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/log.py +0 -0
  43. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/mqlistener_config.py +0 -0
  44. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/mqmsg_model.py +0 -0
  45. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/mqsend_config.py +0 -0
  46. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/models/sso_user.py +0 -0
  47. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/services.py +0 -0
  48. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/sse/__init__.py +0 -0
  49. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/sse/event.py +0 -0
  50. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/sse/sse.py +0 -0
  51. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/__init__.py +0 -0
  52. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/example.py +0 -0
  53. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/example2.py +0 -0
  54. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/feign.py +0 -0
  55. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/feign_client.py +0 -0
  56. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/nacos_service.py +0 -0
  57. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/synacos/param.py +0 -0
  58. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/tools/__init__.py +0 -0
  59. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/tools/docs.py +0 -0
  60. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/tools/snowflake.py +0 -0
  61. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon/tools/timing.py +0 -0
  62. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
  63. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  64. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  65. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
  66. {sycommon_python_lib-0.1.40 → sycommon_python_lib-0.1.42}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.40
3
+ Version: 0.1.42
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.40"
3
+ version = "0.1.42"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,379 @@
1
+ from aio_pika import Channel
2
+ from typing import Optional
3
+ import asyncio
4
+ import json
5
+ from typing import Callable, Coroutine, Dict, Any, Union
6
+ from aio_pika import Message, DeliveryMode, ExchangeType
7
+ from aio_pika.abc import (
8
+ AbstractExchange,
9
+ AbstractQueue,
10
+ AbstractIncomingMessage,
11
+ ConsumerTag,
12
+ AbstractRobustConnection
13
+ )
14
+ from sycommon.rabbitmq.rabbitmq_pool import RabbitMQConnectionPool
15
+ from sycommon.logging.kafka_log import SYLogger
16
+ from sycommon.models.mqmsg_model import MQMsgModel
17
+
18
+
19
+ # 最大重试次数限制
20
+ MAX_RETRY_COUNT = 3
21
+
22
+ logger = SYLogger
23
+
24
+
25
+ class RabbitMQClient:
26
+ """
27
+ RabbitMQ 客户端(支持消息发布、消费、自动重连、异常重试)
28
+ 核心特性:
29
+ 1. 基于连接池复用资源,性能优化
30
+ 2. 连接/通道失效时自动重建,高可用
31
+ 3. 消息发布支持重试,消费支持手动ACK/NACK
32
+ 4. 兼容JSON/字符串/字典消息格式
33
+ 5. 严格的协程调用规范,避免"未等待"警告
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ connection_pool: RabbitMQConnectionPool,
39
+ exchange_name: str = "system.topic.exchange",
40
+ exchange_type: str = "topic",
41
+ queue_name: Optional[str] = None,
42
+ routing_key: str = "#",
43
+ durable: bool = True,
44
+ auto_delete: bool = False,
45
+ auto_parse_json: bool = True,
46
+ create_if_not_exists: bool = True,
47
+ prefetch_count: int = 2,
48
+ # 兼容旧代码参数(无需使用)
49
+ **kwargs,
50
+ ):
51
+ # 依赖注入:连接池(必须已初始化)
52
+ self.connection_pool = connection_pool
53
+ if not self.connection_pool._initialized:
54
+ raise RuntimeError("连接池未初始化,请先调用 connection_pool.init_pools()")
55
+
56
+ # 交换机配置
57
+ self.exchange_name = exchange_name.strip()
58
+ try:
59
+ self.exchange_type = ExchangeType(exchange_type.lower())
60
+ except ValueError:
61
+ SYLogger.warning(f"无效的exchange_type: {exchange_type},默认使用'topic'")
62
+ self.exchange_type = ExchangeType.topic
63
+
64
+ # 队列配置
65
+ self.queue_name = queue_name.strip() if queue_name else None
66
+ self.routing_key = routing_key.strip() if routing_key else "#"
67
+ self.durable = durable # 消息/队列持久化
68
+ self.auto_delete = auto_delete # 无消费者时自动删除队列/交换机
69
+ self.auto_parse_json = auto_parse_json # 自动解析JSON消息体
70
+ self.create_if_not_exists = create_if_not_exists # 不存在则创建交换机/队列
71
+
72
+ # 消费配置
73
+ self.prefetch_count = max(1, prefetch_count) # 每次预取消息数(避免消息堆积)
74
+
75
+ # 内部状态(资源+连接)
76
+ self._channel: Optional[Channel] = None
77
+ self._channel_conn: Optional[AbstractRobustConnection] = None # 通道所属连接
78
+ self._exchange: Optional[AbstractExchange] = None
79
+ self._queue: Optional[AbstractQueue] = None
80
+ self._consumer_tag: Optional[ConsumerTag] = None
81
+ self._message_handler: Optional[Callable[[
82
+ MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]]] = None
83
+ self._closed = False
84
+
85
+ # 线程安全锁
86
+ self._consume_lock = asyncio.Lock()
87
+ self._connect_lock = asyncio.Lock()
88
+
89
+ @property
90
+ async def is_connected(self) -> bool:
91
+ """异步检查客户端连接状态(属性,不可调用)"""
92
+ if self._closed:
93
+ return False
94
+ try:
95
+ # 校验通道+连接+核心资源都有效
96
+ return (
97
+ self._channel and not self._channel.is_closed
98
+ and self._channel_conn and not self._channel_conn.is_closed
99
+ and self._exchange is not None
100
+ and (not self.queue_name or self._queue is not None)
101
+ )
102
+ except Exception as e:
103
+ SYLogger.warning(f"检查连接状态失败: {str(e)}")
104
+ return False
105
+
106
+ async def connect(self) -> None:
107
+ """建立连接并初始化交换机/队列(支持重连)"""
108
+ if self._closed:
109
+ raise RuntimeError("客户端已关闭,无法重新连接")
110
+
111
+ async with self._connect_lock:
112
+ # 释放旧的无效资源
113
+ if self._channel and self._channel_conn:
114
+ try:
115
+ await self.connection_pool.release_channel(self._channel, self._channel_conn)
116
+ except Exception as e:
117
+ SYLogger.warning(f"释放旧通道失败: {str(e)}")
118
+ self._channel = None
119
+ self._channel_conn = None
120
+ self._exchange = None
121
+ self._queue = None
122
+
123
+ try:
124
+ # 1. 从连接池获取通道+连接
125
+ self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
126
+
127
+ def on_conn_closed(conn: AbstractRobustConnection, exc: Optional[BaseException]):
128
+ """连接关闭时触发的回调"""
129
+ SYLogger.error(
130
+ f"客户端连接关闭: {conn!r},原因: {exc}", exc_info=exc)
131
+ if not self._closed:
132
+ asyncio.create_task(self.connect())
133
+
134
+ # 给连接添加关闭回调
135
+ if self._channel_conn:
136
+ self._channel_conn.close_callbacks.add(on_conn_closed)
137
+
138
+ # 2. 设置预取计数(限流)
139
+ await self._channel.set_qos(prefetch_count=self.prefetch_count)
140
+ SYLogger.debug(f"设置预取计数: {self.prefetch_count}")
141
+
142
+ # 3. 声明交换机
143
+ self._exchange = await self._channel.declare_exchange(
144
+ name=self.exchange_name,
145
+ type=self.exchange_type,
146
+ durable=self.durable,
147
+ auto_delete=self.auto_delete,
148
+ passive=not self.create_if_not_exists, # passive=True时,不存在则报错
149
+ )
150
+ SYLogger.info(
151
+ f"交换机初始化成功: {self.exchange_name}(类型: {self.exchange_type.value})")
152
+
153
+ # 4. 声明队列(如果配置了队列名)
154
+ if self.queue_name:
155
+ self._queue = await self._channel.declare_queue(
156
+ name=self.queue_name,
157
+ durable=self.durable,
158
+ auto_delete=self.auto_delete,
159
+ passive=not self.create_if_not_exists,
160
+ )
161
+ # 绑定队列到交换机
162
+ await self._queue.bind(
163
+ exchange=self._exchange,
164
+ routing_key=self.routing_key,
165
+ )
166
+ SYLogger.info(
167
+ f"队列初始化成功: {self.queue_name} "
168
+ f"(绑定交换机: {self.exchange_name}, routing_key: {self.routing_key})"
169
+ )
170
+
171
+ SYLogger.info("客户端连接初始化完成")
172
+ except Exception as e:
173
+ SYLogger.error(f"客户端连接失败: {str(e)}", exc_info=True)
174
+ # 清理异常状态
175
+ if self._channel and self._channel_conn:
176
+ try:
177
+ await self.connection_pool.release_channel(self._channel, self._channel_conn)
178
+ except:
179
+ pass
180
+ self._channel = None
181
+ self._channel_conn = None
182
+ raise
183
+
184
+ async def set_message_handler(
185
+ self,
186
+ handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine[Any, Any, None]],
187
+ ) -> None:
188
+ """设置消息处理器(必须是协程函数)"""
189
+ if not asyncio.iscoroutinefunction(handler):
190
+ raise TypeError("消息处理器必须是协程函数(使用 async def 定义)")
191
+
192
+ async with self._consume_lock:
193
+ self._message_handler = handler
194
+ SYLogger.info("消息处理器设置成功")
195
+
196
+ async def start_consuming(self) -> Optional[ConsumerTag]:
197
+ """启动消息消费(支持自动重连)"""
198
+ if self._closed:
199
+ raise RuntimeError("客户端已关闭,无法启动消费")
200
+
201
+ async with self._consume_lock:
202
+ # 1. 校验前置条件
203
+ if not self._message_handler:
204
+ raise RuntimeError("未设置消息处理器,请先调用 set_message_handler()")
205
+ if not await self.is_connected:
206
+ await self.connect()
207
+ if not self._queue:
208
+ raise RuntimeError("未配置队列名,无法启动消费")
209
+
210
+ # 2. 定义消费回调(包含异常处理和重连逻辑)
211
+ async def consume_callback(message: AbstractIncomingMessage):
212
+ try:
213
+ # 解析消息体
214
+ if self.auto_parse_json:
215
+ try:
216
+ body_dict = json.loads(
217
+ message.body.decode("utf-8"))
218
+ msg_obj = MQMsgModel(**body_dict)
219
+ except json.JSONDecodeError as e:
220
+ SYLogger.error(
221
+ f"JSON消息解析失败: {str(e)},消息体: {message.body[:100]}...")
222
+ await message.nack(requeue=False) # 解析失败,不重入队
223
+ return
224
+ else:
225
+ msg_obj = MQMsgModel(
226
+ body=message.body.decode("utf-8"),
227
+ routing_key=message.routing_key,
228
+ delivery_tag=message.delivery_tag,
229
+ )
230
+
231
+ # 调用消息处理器(必须await,避免协程未等待警告)
232
+ await self._message_handler(msg_obj, message)
233
+
234
+ # 手动ACK(消息处理成功)
235
+ await message.ack()
236
+ SYLogger.debug(
237
+ f"消息处理成功,delivery_tag: {message.delivery_tag}")
238
+
239
+ except Exception as e:
240
+ SYLogger.error(
241
+ f"消息处理失败,delivery_tag: {message.delivery_tag}",
242
+ exc_info=True
243
+ )
244
+ # 处理失败逻辑:首次失败重入队,再次失败丢弃
245
+ if message.redelivered:
246
+ SYLogger.warning(
247
+ f"消息已重入队过,本次拒绝入队: {message.delivery_tag}")
248
+ await message.reject(requeue=False)
249
+ else:
250
+ SYLogger.warning(f"消息重入队: {message.delivery_tag}")
251
+ await message.nack(requeue=True)
252
+
253
+ # 检查连接状态,失效则触发重连
254
+ if not await self.is_connected:
255
+ SYLogger.warning("连接已失效,触发客户端重连")
256
+ asyncio.create_task(self.connect())
257
+
258
+ # 3. 启动消费
259
+ self._consumer_tag = await self._queue.consume(consume_callback)
260
+ SYLogger.info(
261
+ f"开始消费队列: {self._queue.name},consumer_tag: {self._consumer_tag}"
262
+ )
263
+ return self._consumer_tag
264
+
265
+ async def stop_consuming(self) -> None:
266
+ """停止消息消费"""
267
+ async with self._consume_lock:
268
+ if self._consumer_tag and self._queue and not self._queue.is_closed:
269
+ try:
270
+ await self._queue.cancel(self._consumer_tag)
271
+ SYLogger.info(f"停止消费成功,consumer_tag: {self._consumer_tag}")
272
+ except Exception as e:
273
+ SYLogger.error(f"停止消费失败: {str(e)}", exc_info=True)
274
+ finally:
275
+ self._consumer_tag = None
276
+
277
+ async def publish(
278
+ self,
279
+ message_body: Union[str, Dict[str, Any], MQMsgModel],
280
+ headers: Optional[Dict[str, Any]] = None,
281
+ content_type: str = "application/json",
282
+ delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT,
283
+ retry_count: int = 3,
284
+ ) -> None:
285
+ """
286
+ 发布消息(支持自动重试、JSON序列化)
287
+ :param message_body: 消息体(字符串/字典/MQMsgModel)
288
+ :param headers: 消息头(可选)
289
+ :param content_type: 内容类型(默认application/json)
290
+ :param delivery_mode: 投递模式(PERSISTENT=持久化,TRANSIENT=非持久化)
291
+ :param retry_count: 重试次数(默认3次)
292
+ """
293
+ if self._closed:
294
+ raise RuntimeError("客户端已关闭,无法发布消息")
295
+
296
+ # 处理消息体序列化
297
+ try:
298
+ if isinstance(message_body, MQMsgModel):
299
+ body = json.dumps(message_body.to_dict(),
300
+ ensure_ascii=False).encode("utf-8")
301
+ elif isinstance(message_body, dict):
302
+ body = json.dumps(
303
+ message_body, ensure_ascii=False).encode("utf-8")
304
+ elif isinstance(message_body, str):
305
+ body = message_body.encode("utf-8")
306
+ else:
307
+ raise TypeError(f"不支持的消息体类型: {type(message_body)}")
308
+ except Exception as e:
309
+ SYLogger.error(f"消息体序列化失败: {str(e)}", exc_info=True)
310
+ raise
311
+
312
+ # 构建消息对象
313
+ message = Message(
314
+ body=body,
315
+ headers=headers or {},
316
+ content_type=content_type,
317
+ delivery_mode=delivery_mode,
318
+ )
319
+
320
+ # 发布重试逻辑
321
+ for retry in range(retry_count):
322
+ try:
323
+ # 确保连接有效
324
+ if not await self.is_connected:
325
+ SYLogger.warning(f"发布消息前连接失效,触发重连(retry: {retry})")
326
+ await self.connect()
327
+
328
+ # 发布消息
329
+ await self._exchange.publish(
330
+ message=message,
331
+ routing_key=self.routing_key or self.queue_name or "#",
332
+ )
333
+ SYLogger.info(
334
+ f"消息发布成功(retry: {retry}),routing_key: {self.routing_key},"
335
+ f"delivery_mode: {delivery_mode.value}"
336
+ )
337
+ return
338
+ except Exception as e:
339
+ SYLogger.error(
340
+ f"消息发布失败(retry: {retry}/{retry_count-1}): {str(e)}",
341
+ exc_info=True
342
+ )
343
+ # 清理失效状态,下次重试时重连
344
+ self._exchange = None
345
+ # 重试间隔(指数退避)
346
+ await asyncio.sleep(0.5 * (2 ** retry))
347
+
348
+ # 所有重试失败,抛出异常
349
+ raise RuntimeError(
350
+ f"消息发布失败(已重试{retry_count}次),routing_key: {self.routing_key}"
351
+ )
352
+
353
+ async def close(self) -> None:
354
+ """关闭客户端(释放资源)"""
355
+ if self._closed:
356
+ SYLogger.warning("客户端已关闭,无需重复操作")
357
+ return
358
+
359
+ self._closed = True
360
+ SYLogger.info("开始关闭RabbitMQ客户端...")
361
+
362
+ # 1. 停止消费
363
+ await self.stop_consuming()
364
+
365
+ # 2. 释放通道到连接池
366
+ async with self._connect_lock:
367
+ if self._channel and self._channel_conn:
368
+ try:
369
+ await self.connection_pool.release_channel(self._channel, self._channel_conn)
370
+ SYLogger.info("通道释放成功")
371
+ except Exception as e:
372
+ SYLogger.error(f"通道释放失败: {str(e)}", exc_info=True)
373
+ self._channel = None
374
+ self._channel_conn = None
375
+ self._exchange = None
376
+ self._queue = None
377
+ self._message_handler = None
378
+
379
+ SYLogger.info("RabbitMQ客户端已完全关闭")