sycommon-python-lib 0.1.0__py3-none-any.whl → 0.1.2__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/EmbeddingConfig.py +0 -1
- sycommon/config/MQConfig.py +15 -0
- sycommon/database/base_db_service.py +30 -0
- sycommon/logging/kafka_log.py +14 -38
- sycommon/middleware/middleware.py +4 -0
- sycommon/middleware/mq.py +9 -0
- sycommon/models/mqlistener_config.py +38 -0
- sycommon/models/mqmsg_model.py +11 -0
- sycommon/models/mqsend_config.py +8 -0
- sycommon/models/sso_user.py +60 -0
- sycommon/rabbitmq/rabbitmq_client.py +721 -0
- sycommon/rabbitmq/rabbitmq_service.py +476 -0
- sycommon/services.py +164 -20
- sycommon/synacos/feign.py +14 -10
- sycommon/synacos/nacos_service.py +252 -188
- {sycommon_python_lib-0.1.0.dist-info → sycommon_python_lib-0.1.2.dist-info}/METADATA +2 -2
- {sycommon_python_lib-0.1.0.dist-info → sycommon_python_lib-0.1.2.dist-info}/RECORD +19 -10
- {sycommon_python_lib-0.1.0.dist-info → sycommon_python_lib-0.1.2.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.0.dist-info → sycommon_python_lib-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import aio_pika
|
|
4
|
+
import json
|
|
5
|
+
from aio_pika.abc import AbstractIncomingMessage, ExchangeType
|
|
6
|
+
from typing import Callable, Coroutine, Optional, Dict, Any, Union
|
|
7
|
+
|
|
8
|
+
from sycommon.models.mqmsg_model import MQMsgModel
|
|
9
|
+
from aiormq.exceptions import ChannelInvalidStateError, ConnectionClosed
|
|
10
|
+
|
|
11
|
+
# 最大重试次数限制
|
|
12
|
+
MAX_RETRY_COUNT = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RabbitMQClient:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
host: str,
|
|
19
|
+
port: int,
|
|
20
|
+
username: str,
|
|
21
|
+
password: str,
|
|
22
|
+
virtualhost: str = "/",
|
|
23
|
+
exchange_name: str = "system.topic.exchange",
|
|
24
|
+
exchange_type: str = "topic",
|
|
25
|
+
queue_name: Optional[str] = None,
|
|
26
|
+
routing_key: str = "#",
|
|
27
|
+
durable: bool = True,
|
|
28
|
+
auto_delete: bool = False,
|
|
29
|
+
auto_parse_json: bool = True,
|
|
30
|
+
create_if_not_exists: bool = True,
|
|
31
|
+
connection_timeout: int = 10,
|
|
32
|
+
rpc_timeout: int = 10,
|
|
33
|
+
app_name: str = "",
|
|
34
|
+
reconnection_delay: int = 3,
|
|
35
|
+
max_reconnection_attempts: int = 5,
|
|
36
|
+
heartbeat: int = 30,
|
|
37
|
+
keepalive_interval: int = 15
|
|
38
|
+
):
|
|
39
|
+
"""初始化RabbitMQ客户端,增加心跳和保活配置"""
|
|
40
|
+
self.host = host
|
|
41
|
+
self.port = port
|
|
42
|
+
self.username = username
|
|
43
|
+
self.password = password
|
|
44
|
+
self.virtualhost = virtualhost
|
|
45
|
+
self.exchange_name = exchange_name
|
|
46
|
+
self.exchange_type = ExchangeType(exchange_type)
|
|
47
|
+
self.queue_name = queue_name
|
|
48
|
+
self.routing_key = routing_key
|
|
49
|
+
self.durable = durable
|
|
50
|
+
self.auto_delete = auto_delete
|
|
51
|
+
self.auto_parse_json = auto_parse_json
|
|
52
|
+
self.create_if_not_exists = create_if_not_exists
|
|
53
|
+
self.connection_timeout = connection_timeout
|
|
54
|
+
self.rpc_timeout = rpc_timeout
|
|
55
|
+
self.app_name = app_name
|
|
56
|
+
|
|
57
|
+
# 连接保活相关配置
|
|
58
|
+
self.heartbeat = heartbeat
|
|
59
|
+
self.keepalive_interval = keepalive_interval
|
|
60
|
+
self.last_activity_timestamp = asyncio.get_event_loop().time()
|
|
61
|
+
|
|
62
|
+
# 重连相关配置
|
|
63
|
+
self.reconnection_delay = reconnection_delay
|
|
64
|
+
self.max_reconnection_attempts = max_reconnection_attempts
|
|
65
|
+
|
|
66
|
+
# 连接和通道相关属性
|
|
67
|
+
self.connection: Optional[aio_pika.RobustConnection] = None
|
|
68
|
+
self.channel: Optional[aio_pika.RobustChannel] = None
|
|
69
|
+
self.exchange: Optional[aio_pika.Exchange] = None
|
|
70
|
+
self.queue: Optional[aio_pika.Queue] = None
|
|
71
|
+
|
|
72
|
+
self._exchange_exists = False
|
|
73
|
+
self._queue_exists = False
|
|
74
|
+
self._queue_bound = False
|
|
75
|
+
|
|
76
|
+
# 消息处理器
|
|
77
|
+
self.message_handler: Optional[Callable[
|
|
78
|
+
[Union[AbstractIncomingMessage, Dict[str, Any]], AbstractIncomingMessage],
|
|
79
|
+
Coroutine
|
|
80
|
+
]] = None
|
|
81
|
+
|
|
82
|
+
# 消费相关
|
|
83
|
+
self._consumer_tag: Optional[str] = None
|
|
84
|
+
self._consuming_task: Optional[asyncio.Task] = None
|
|
85
|
+
self._is_consuming: bool = False
|
|
86
|
+
self._reconnect_task: Optional[asyncio.Task] = None
|
|
87
|
+
self._closed: bool = False
|
|
88
|
+
self._keepalive_task: Optional[asyncio.Task] = None # 保活任务
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def is_connected(self) -> bool:
|
|
92
|
+
"""检查连接是否有效"""
|
|
93
|
+
return (not self._closed and
|
|
94
|
+
self.connection is not None and
|
|
95
|
+
not self.connection.is_closed and
|
|
96
|
+
self.channel is not None and
|
|
97
|
+
not self.channel.is_closed)
|
|
98
|
+
|
|
99
|
+
def _update_activity_timestamp(self):
|
|
100
|
+
"""更新最后活动时间戳"""
|
|
101
|
+
self.last_activity_timestamp = asyncio.get_event_loop().time()
|
|
102
|
+
|
|
103
|
+
async def _check_exchange_exists(self) -> bool:
|
|
104
|
+
"""检查交换机是否存在,增加超时控制"""
|
|
105
|
+
if not self.channel:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
await asyncio.wait_for(
|
|
110
|
+
self.channel.declare_exchange(
|
|
111
|
+
name=self.exchange_name,
|
|
112
|
+
type=self.exchange_type,
|
|
113
|
+
passive=True
|
|
114
|
+
),
|
|
115
|
+
timeout=self.rpc_timeout
|
|
116
|
+
)
|
|
117
|
+
self._exchange_exists = True
|
|
118
|
+
self._update_activity_timestamp()
|
|
119
|
+
return True
|
|
120
|
+
except asyncio.TimeoutError:
|
|
121
|
+
logging.error(f"检查交换机 '{self.exchange_name}' 超时")
|
|
122
|
+
return False
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logging.debug(f"交换机 '{self.exchange_name}' 不存在: {str(e)}")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
async def _check_queue_exists(self) -> bool:
|
|
128
|
+
"""检查队列是否存在,增加超时控制"""
|
|
129
|
+
if not self.channel or not self.queue_name:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
await asyncio.wait_for(
|
|
134
|
+
self.channel.declare_queue(
|
|
135
|
+
name=self.queue_name,
|
|
136
|
+
passive=True
|
|
137
|
+
),
|
|
138
|
+
timeout=self.rpc_timeout
|
|
139
|
+
)
|
|
140
|
+
self._queue_exists = True
|
|
141
|
+
self._update_activity_timestamp()
|
|
142
|
+
return True
|
|
143
|
+
except asyncio.TimeoutError:
|
|
144
|
+
logging.error(f"检查队列 '{self.queue_name}' 超时")
|
|
145
|
+
return False
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logging.debug(f"队列 '{self.queue_name}' 不存在: {str(e)}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
async def _bind_queue(self) -> bool:
|
|
151
|
+
"""绑定队列到交换机,增加超时控制和重试"""
|
|
152
|
+
if not self.channel or not self.queue or not self.exchange:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
retries = 2 # 绑定操作重试次数
|
|
156
|
+
for attempt in range(retries + 1):
|
|
157
|
+
try:
|
|
158
|
+
bind_routing_key = self.routing_key if self.routing_key else '#'
|
|
159
|
+
await asyncio.wait_for(
|
|
160
|
+
self.queue.bind(
|
|
161
|
+
self.exchange,
|
|
162
|
+
routing_key=bind_routing_key
|
|
163
|
+
),
|
|
164
|
+
timeout=self.rpc_timeout
|
|
165
|
+
)
|
|
166
|
+
self._queue_bound = True
|
|
167
|
+
self._update_activity_timestamp()
|
|
168
|
+
logging.info(
|
|
169
|
+
f"队列 '{self.queue_name}' 已绑定到交换机 '{self.exchange_name}',路由键: {bind_routing_key}")
|
|
170
|
+
return True
|
|
171
|
+
except asyncio.TimeoutError:
|
|
172
|
+
logging.warning(
|
|
173
|
+
f"队列 '{self.queue_name}' 绑定超时(第{attempt+1}次尝试)")
|
|
174
|
+
if attempt >= retries:
|
|
175
|
+
self._queue_bound = False
|
|
176
|
+
return False
|
|
177
|
+
await asyncio.sleep(1)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logging.error(f"队列绑定失败(第{attempt+1}次尝试): {str(e)}")
|
|
180
|
+
if attempt >= retries:
|
|
181
|
+
self._queue_bound = False
|
|
182
|
+
return False
|
|
183
|
+
await asyncio.sleep(1)
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
async def connect(self, force_reconnect: bool = False, max_retries: int = 3) -> None:
|
|
187
|
+
"""建立连接并检查/创建资源"""
|
|
188
|
+
if self.is_connected and not force_reconnect:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# 如果正在重连,先取消
|
|
192
|
+
if self._reconnect_task and not self._reconnect_task.done():
|
|
193
|
+
self._reconnect_task.cancel()
|
|
194
|
+
|
|
195
|
+
logging.debug(
|
|
196
|
+
f"尝试连接RabbitMQ - 主机: {self.host}:{self.port}, "
|
|
197
|
+
f"虚拟主机: {self.virtualhost}, "
|
|
198
|
+
f"队列: {self.queue_name}, "
|
|
199
|
+
f"心跳间隔: {self.heartbeat}s"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# 重置状态
|
|
203
|
+
self._exchange_exists = False
|
|
204
|
+
self._queue_exists = False
|
|
205
|
+
self._queue_bound = False
|
|
206
|
+
|
|
207
|
+
retries = 0
|
|
208
|
+
last_exception = None
|
|
209
|
+
|
|
210
|
+
while retries < max_retries:
|
|
211
|
+
try:
|
|
212
|
+
# 关闭旧连接
|
|
213
|
+
if self.connection and not self.connection.is_closed:
|
|
214
|
+
await self.connection.close()
|
|
215
|
+
|
|
216
|
+
# 建立新连接 - 配置更短的心跳间隔
|
|
217
|
+
self.connection = await asyncio.wait_for(
|
|
218
|
+
aio_pika.connect_robust(
|
|
219
|
+
host=self.host,
|
|
220
|
+
port=self.port,
|
|
221
|
+
login=self.username,
|
|
222
|
+
password=self.password,
|
|
223
|
+
virtualhost=self.virtualhost,
|
|
224
|
+
heartbeat=self.heartbeat,
|
|
225
|
+
client_properties={
|
|
226
|
+
"connection_name": self.app_name or "rabbitmq-client"} # 增加连接标识
|
|
227
|
+
),
|
|
228
|
+
timeout=self.connection_timeout
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# 创建通道
|
|
232
|
+
self.channel = await asyncio.wait_for(
|
|
233
|
+
self.connection.channel(),
|
|
234
|
+
timeout=self.rpc_timeout
|
|
235
|
+
)
|
|
236
|
+
await self.channel.set_qos(prefetch_count=2)
|
|
237
|
+
|
|
238
|
+
# 1. 处理交换机
|
|
239
|
+
exchange_exists = await self._check_exchange_exists()
|
|
240
|
+
if not exchange_exists:
|
|
241
|
+
if self.create_if_not_exists:
|
|
242
|
+
# 创建交换机
|
|
243
|
+
self.exchange = await asyncio.wait_for(
|
|
244
|
+
self.channel.declare_exchange(
|
|
245
|
+
name=self.exchange_name,
|
|
246
|
+
type=self.exchange_type,
|
|
247
|
+
durable=self.durable,
|
|
248
|
+
auto_delete=self.auto_delete
|
|
249
|
+
),
|
|
250
|
+
timeout=self.rpc_timeout
|
|
251
|
+
)
|
|
252
|
+
self._exchange_exists = True
|
|
253
|
+
logging.info(f"已创建交换机 '{self.exchange_name}'")
|
|
254
|
+
else:
|
|
255
|
+
raise Exception(
|
|
256
|
+
f"交换机 '{self.exchange_name}' 不存在且不允许自动创建")
|
|
257
|
+
else:
|
|
258
|
+
# 获取已有交换机
|
|
259
|
+
self.exchange = await asyncio.wait_for(
|
|
260
|
+
self.channel.get_exchange(self.exchange_name),
|
|
261
|
+
timeout=self.rpc_timeout
|
|
262
|
+
)
|
|
263
|
+
logging.info(f"使用已存在的交换机 '{self.exchange_name}'")
|
|
264
|
+
|
|
265
|
+
# 2. 处理队列
|
|
266
|
+
if self.queue_name:
|
|
267
|
+
queue_exists = await self._check_queue_exists()
|
|
268
|
+
|
|
269
|
+
if not queue_exists:
|
|
270
|
+
if self.create_if_not_exists:
|
|
271
|
+
# 创建队列
|
|
272
|
+
self.queue = await asyncio.wait_for(
|
|
273
|
+
self.channel.declare_queue(
|
|
274
|
+
name=self.queue_name,
|
|
275
|
+
durable=self.durable,
|
|
276
|
+
auto_delete=self.auto_delete,
|
|
277
|
+
exclusive=False,
|
|
278
|
+
passive=False
|
|
279
|
+
),
|
|
280
|
+
timeout=self.rpc_timeout
|
|
281
|
+
)
|
|
282
|
+
self._queue_exists = True
|
|
283
|
+
logging.info(f"已创建队列 '{self.queue_name}'")
|
|
284
|
+
else:
|
|
285
|
+
raise Exception(
|
|
286
|
+
f"队列 '{self.queue_name}' 不存在且不允许自动创建")
|
|
287
|
+
else:
|
|
288
|
+
# 获取已有队列
|
|
289
|
+
self.queue = await asyncio.wait_for(
|
|
290
|
+
self.channel.get_queue(self.queue_name),
|
|
291
|
+
timeout=self.rpc_timeout
|
|
292
|
+
)
|
|
293
|
+
logging.info(f"使用已存在的队列 '{self.queue_name}'")
|
|
294
|
+
|
|
295
|
+
# 3. 绑定队列到交换机
|
|
296
|
+
if self.queue and self.exchange:
|
|
297
|
+
bound = await self._bind_queue()
|
|
298
|
+
if not bound:
|
|
299
|
+
raise Exception(
|
|
300
|
+
f"队列 '{self.queue_name}' 绑定到交换机 '{self.exchange_name}' 失败")
|
|
301
|
+
|
|
302
|
+
# 如果之前在消费,重新开始消费
|
|
303
|
+
if self._is_consuming and self.message_handler:
|
|
304
|
+
await self.start_consuming()
|
|
305
|
+
|
|
306
|
+
# 启动连接监控和保活任务
|
|
307
|
+
self._start_connection_monitor()
|
|
308
|
+
self._start_keepalive_task()
|
|
309
|
+
|
|
310
|
+
self._update_activity_timestamp()
|
|
311
|
+
logging.info(f"RabbitMQ客户端连接成功 (队列: {self.queue_name})")
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
retries += 1
|
|
316
|
+
last_exception = e
|
|
317
|
+
logging.warning(
|
|
318
|
+
f"连接失败({retries}/{max_retries}): {str(e)},重试中...")
|
|
319
|
+
|
|
320
|
+
if retries < max_retries:
|
|
321
|
+
await asyncio.sleep(self.reconnection_delay)
|
|
322
|
+
|
|
323
|
+
logging.error(f"最终连接失败: {str(last_exception)}", exc_info=True)
|
|
324
|
+
raise Exception(
|
|
325
|
+
f"经过{max_retries}次重试后仍无法完成连接和资源初始化。"
|
|
326
|
+
f"最后错误: {str(last_exception)}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _start_connection_monitor(self):
|
|
330
|
+
"""启动连接监控任务,检测连接/通道关闭"""
|
|
331
|
+
if self._closed:
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
async def monitor_task():
|
|
335
|
+
while not self._closed and self.connection:
|
|
336
|
+
try:
|
|
337
|
+
# 检查连接状态
|
|
338
|
+
if self.connection.is_closed:
|
|
339
|
+
logging.warning("检测到RabbitMQ连接已关闭")
|
|
340
|
+
await self._schedule_reconnect()
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
# 检查通道状态
|
|
344
|
+
if self.channel and self.channel.is_closed:
|
|
345
|
+
logging.warning("检测到RabbitMQ通道已关闭")
|
|
346
|
+
await self._recreate_channel()
|
|
347
|
+
continue
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logging.error(f"连接监控任务出错: {str(e)}")
|
|
350
|
+
await asyncio.sleep(1)
|
|
351
|
+
|
|
352
|
+
await asyncio.sleep(5)
|
|
353
|
+
|
|
354
|
+
# 创建监控任务
|
|
355
|
+
asyncio.create_task(monitor_task())
|
|
356
|
+
|
|
357
|
+
async def _recreate_channel(self):
|
|
358
|
+
"""重建通道并恢复绑定和消费"""
|
|
359
|
+
try:
|
|
360
|
+
if not self.connection or self.connection.is_closed:
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
# 重新创建通道
|
|
364
|
+
self.channel = await self.connection.channel()
|
|
365
|
+
await self.channel.set_qos(prefetch_count=2)
|
|
366
|
+
|
|
367
|
+
# 重新绑定队列和交换机
|
|
368
|
+
if self.queue and self.exchange:
|
|
369
|
+
await self._bind_queue()
|
|
370
|
+
|
|
371
|
+
# 重新开始消费
|
|
372
|
+
if self._is_consuming and self.message_handler:
|
|
373
|
+
await self.start_consuming()
|
|
374
|
+
|
|
375
|
+
logging.info("通道已重新创建并恢复服务")
|
|
376
|
+
self._update_activity_timestamp()
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logging.error(f"重建通道失败: {str(e)}")
|
|
379
|
+
await self._schedule_reconnect()
|
|
380
|
+
|
|
381
|
+
def _start_keepalive_task(self):
|
|
382
|
+
"""启动连接保活任务,适配RobustConnection的特性"""
|
|
383
|
+
if self._closed or (self._keepalive_task and not self._keepalive_task.done()):
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
async def keepalive_task():
|
|
387
|
+
while not self._closed and self.is_connected:
|
|
388
|
+
current_time = asyncio.get_event_loop().time()
|
|
389
|
+
# 检查是否超过指定时间无活动
|
|
390
|
+
if current_time - self.last_activity_timestamp > self.heartbeat * 1.5:
|
|
391
|
+
logging.debug(f"连接 {self.heartbeat*1.5}s 无活动,执行保活检查")
|
|
392
|
+
try:
|
|
393
|
+
# 针对RobustConnection的兼容处理
|
|
394
|
+
if self.connection:
|
|
395
|
+
# 检查连接状态
|
|
396
|
+
if self.connection.is_closed:
|
|
397
|
+
logging.warning("连接已关闭,触发重连")
|
|
398
|
+
await self._schedule_reconnect()
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
# 尝试一个轻量级操作来保持连接活跃
|
|
402
|
+
if self.channel:
|
|
403
|
+
# 使用通道声明一个空的交换机(被动模式)作为保活检测
|
|
404
|
+
await asyncio.wait_for(
|
|
405
|
+
self.channel.declare_exchange(
|
|
406
|
+
name=self.exchange_name,
|
|
407
|
+
type=self.exchange_type,
|
|
408
|
+
passive=True # 被动模式不会创建交换机,仅检查存在性
|
|
409
|
+
),
|
|
410
|
+
timeout=5
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
self._update_activity_timestamp()
|
|
414
|
+
except asyncio.TimeoutError:
|
|
415
|
+
logging.warning("保活检查超时,触发重连")
|
|
416
|
+
await self._schedule_reconnect()
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logging.warning(f"保活检查失败: {str(e)},触发重连")
|
|
419
|
+
await self._schedule_reconnect()
|
|
420
|
+
|
|
421
|
+
await asyncio.sleep(self.keepalive_interval)
|
|
422
|
+
|
|
423
|
+
self._keepalive_task = asyncio.create_task(keepalive_task())
|
|
424
|
+
|
|
425
|
+
async def _schedule_reconnect(self):
|
|
426
|
+
"""安排重新连接"""
|
|
427
|
+
if self._reconnect_task and not self._reconnect_task.done():
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
logging.info(f"将在 {self.reconnection_delay} 秒后尝试重新连接...")
|
|
431
|
+
|
|
432
|
+
async def reconnect_task():
|
|
433
|
+
try:
|
|
434
|
+
await asyncio.sleep(self.reconnection_delay)
|
|
435
|
+
if not self._closed:
|
|
436
|
+
await self.connect(force_reconnect=True, max_retries=self.max_reconnection_attempts)
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logging.error(f"重连任务失败: {str(e)}")
|
|
439
|
+
# 如果重连失败,再次安排重连
|
|
440
|
+
if not self._closed:
|
|
441
|
+
await self._schedule_reconnect()
|
|
442
|
+
|
|
443
|
+
self._reconnect_task = asyncio.create_task(reconnect_task())
|
|
444
|
+
|
|
445
|
+
async def close(self) -> None:
|
|
446
|
+
"""关闭连接,清理所有任务"""
|
|
447
|
+
self._closed = True
|
|
448
|
+
self._is_consuming = False
|
|
449
|
+
|
|
450
|
+
# 取消保活任务
|
|
451
|
+
if self._keepalive_task and not self._keepalive_task.done():
|
|
452
|
+
self._keepalive_task.cancel()
|
|
453
|
+
|
|
454
|
+
# 取消重连任务
|
|
455
|
+
if self._reconnect_task and not self._reconnect_task.done():
|
|
456
|
+
self._reconnect_task.cancel()
|
|
457
|
+
|
|
458
|
+
# 停止消费
|
|
459
|
+
if self._consuming_task and not self._consuming_task.done():
|
|
460
|
+
self._consuming_task.cancel()
|
|
461
|
+
|
|
462
|
+
# 关闭连接
|
|
463
|
+
if self.connection and not self.connection.is_closed:
|
|
464
|
+
try:
|
|
465
|
+
await asyncio.wait_for(self.connection.close(), timeout=5)
|
|
466
|
+
except Exception as e:
|
|
467
|
+
logging.warning(f"关闭连接时出错: {str(e)}")
|
|
468
|
+
|
|
469
|
+
# 重置状态
|
|
470
|
+
self.connection = None
|
|
471
|
+
self.channel = None
|
|
472
|
+
self.exchange = None
|
|
473
|
+
self.queue = None
|
|
474
|
+
self._exchange_exists = False
|
|
475
|
+
self._queue_exists = False
|
|
476
|
+
self._queue_bound = False
|
|
477
|
+
self._consumer_tag = None
|
|
478
|
+
self._consuming_task = None
|
|
479
|
+
self._keepalive_task = None
|
|
480
|
+
|
|
481
|
+
async def send_message(
|
|
482
|
+
self,
|
|
483
|
+
message_body: Union[str, Dict[str, Any]],
|
|
484
|
+
content_type: str = "application/json",
|
|
485
|
+
headers: Optional[Dict[str, Any]] = None
|
|
486
|
+
) -> None:
|
|
487
|
+
"""发送消息到RabbitMQ,带连接检查和重试机制"""
|
|
488
|
+
if not self.is_connected:
|
|
489
|
+
logging.warning("连接已关闭,尝试重新连接后发送消息")
|
|
490
|
+
await self.connect(force_reconnect=True)
|
|
491
|
+
|
|
492
|
+
if not self.channel or not self.exchange:
|
|
493
|
+
raise Exception("RabbitMQ连接未初始化")
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
if isinstance(message_body, dict):
|
|
497
|
+
message_body_str = json.dumps(message_body, ensure_ascii=False)
|
|
498
|
+
if content_type == "text/plain":
|
|
499
|
+
content_type = "application/json"
|
|
500
|
+
else:
|
|
501
|
+
message_body_str = str(message_body)
|
|
502
|
+
|
|
503
|
+
message = aio_pika.Message(
|
|
504
|
+
headers=headers,
|
|
505
|
+
body=message_body_str.encode(),
|
|
506
|
+
content_type=content_type,
|
|
507
|
+
delivery_mode=aio_pika.DeliveryMode.PERSISTENT if self.durable else aio_pika.DeliveryMode.TRANSIENT
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
await self.exchange.publish(
|
|
511
|
+
message,
|
|
512
|
+
routing_key=self.routing_key or '#'
|
|
513
|
+
)
|
|
514
|
+
self._update_activity_timestamp() # 更新活动时间
|
|
515
|
+
except (ChannelInvalidStateError, ConnectionClosed) as e:
|
|
516
|
+
logging.warning(f"通道/连接已关闭,消息发送失败: {str(e)}")
|
|
517
|
+
await self._recreate_channel()
|
|
518
|
+
raise
|
|
519
|
+
except Exception as e:
|
|
520
|
+
logging.warning(f"消息发送失败,尝试重连后再次发送: {str(e)}")
|
|
521
|
+
# 尝试重连
|
|
522
|
+
await self.connect(force_reconnect=True)
|
|
523
|
+
# 重连后再次尝试发送
|
|
524
|
+
raise # 让上层处理重发逻辑
|
|
525
|
+
|
|
526
|
+
def set_message_handler(
|
|
527
|
+
self,
|
|
528
|
+
handler: Callable[
|
|
529
|
+
[Union[AbstractIncomingMessage, Dict[str, Any]], AbstractIncomingMessage],
|
|
530
|
+
Coroutine
|
|
531
|
+
]
|
|
532
|
+
) -> None:
|
|
533
|
+
"""设置消息处理函数"""
|
|
534
|
+
self.message_handler = handler
|
|
535
|
+
|
|
536
|
+
async def start_consuming(self, timeout: Optional[float] = None) -> str:
|
|
537
|
+
"""开始消费消息并返回consumer_tag,支持超时控制"""
|
|
538
|
+
if self._is_consuming:
|
|
539
|
+
logging.debug("已经在消费中,返回现有consumer_tag")
|
|
540
|
+
return self._consumer_tag
|
|
541
|
+
|
|
542
|
+
if not self.is_connected:
|
|
543
|
+
await self.connect()
|
|
544
|
+
|
|
545
|
+
if not self.queue:
|
|
546
|
+
raise Exception("队列未初始化")
|
|
547
|
+
|
|
548
|
+
if not self.message_handler:
|
|
549
|
+
raise Exception("未设置消息处理函数")
|
|
550
|
+
|
|
551
|
+
self._is_consuming = True
|
|
552
|
+
|
|
553
|
+
async def consume_task():
|
|
554
|
+
try:
|
|
555
|
+
while self._is_consuming and self.is_connected:
|
|
556
|
+
try:
|
|
557
|
+
# 消费消息
|
|
558
|
+
self._consumer_tag = await self.queue.consume(self._message_wrapper)
|
|
559
|
+
logging.info(f"消费者已启动,tag: {self._consumer_tag}")
|
|
560
|
+
|
|
561
|
+
# 保持消费循环
|
|
562
|
+
while self._is_consuming and self.is_connected:
|
|
563
|
+
await asyncio.sleep(1)
|
|
564
|
+
|
|
565
|
+
# 如果退出循环,取消消费(增加重试逻辑)
|
|
566
|
+
if self._consumer_tag and self.queue and not self.queue.channel.is_closed:
|
|
567
|
+
await self._safe_cancel_consumer()
|
|
568
|
+
|
|
569
|
+
except (ChannelInvalidStateError, ConnectionClosed) as e:
|
|
570
|
+
if self._closed or not self._is_consuming:
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
logging.error(f"通道/连接异常: {str(e)},尝试重建通道")
|
|
574
|
+
await self._recreate_channel()
|
|
575
|
+
await asyncio.sleep(1)
|
|
576
|
+
except Exception as e:
|
|
577
|
+
if self._closed or not self._is_consuming:
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
logging.error(f"消费过程中出错: {str(e)}", exc_info=True)
|
|
581
|
+
# 如果连接仍然有效,等待后重试
|
|
582
|
+
if self.is_connected:
|
|
583
|
+
await asyncio.sleep(self.reconnection_delay)
|
|
584
|
+
else:
|
|
585
|
+
# 连接无效,等待重连
|
|
586
|
+
while not self.is_connected and self._is_consuming and not self._closed:
|
|
587
|
+
await asyncio.sleep(1)
|
|
588
|
+
except asyncio.CancelledError:
|
|
589
|
+
logging.info("消费任务已取消")
|
|
590
|
+
except Exception as e:
|
|
591
|
+
logging.error(f"消费任务出错: {str(e)}", exc_info=True)
|
|
592
|
+
finally:
|
|
593
|
+
self._is_consuming = False
|
|
594
|
+
self._consumer_tag = None
|
|
595
|
+
logging.info("消费任务已结束")
|
|
596
|
+
|
|
597
|
+
# 保存消费任务引用
|
|
598
|
+
self._consuming_task = asyncio.create_task(consume_task())
|
|
599
|
+
return self._consumer_tag
|
|
600
|
+
|
|
601
|
+
async def _safe_cancel_consumer(self, max_retries: int = 3) -> bool:
|
|
602
|
+
"""安全取消消费者,增加重试机制"""
|
|
603
|
+
if not self._consumer_tag or not self.queue:
|
|
604
|
+
return True
|
|
605
|
+
|
|
606
|
+
for attempt in range(max_retries):
|
|
607
|
+
try:
|
|
608
|
+
await asyncio.wait_for(
|
|
609
|
+
self.queue.cancel(self._consumer_tag),
|
|
610
|
+
timeout=self.rpc_timeout
|
|
611
|
+
)
|
|
612
|
+
logging.info(f"消费者 {self._consumer_tag} 已取消")
|
|
613
|
+
return True
|
|
614
|
+
except ChannelInvalidStateError:
|
|
615
|
+
if attempt >= max_retries - 1:
|
|
616
|
+
logging.error(f"取消消费者 {self._consumer_tag} 失败:通道已关闭")
|
|
617
|
+
return False
|
|
618
|
+
logging.warning(f"取消消费者尝试 {attempt+1} 失败,通道状态异常,重试中...")
|
|
619
|
+
await asyncio.sleep(1)
|
|
620
|
+
except asyncio.TimeoutError:
|
|
621
|
+
if attempt >= max_retries - 1:
|
|
622
|
+
logging.error(f"取消消费者 {self._consumer_tag} 超时")
|
|
623
|
+
return False
|
|
624
|
+
logging.warning(f"取消消费者尝试 {attempt+1} 超时,重试中...")
|
|
625
|
+
await asyncio.sleep(1)
|
|
626
|
+
except Exception as e:
|
|
627
|
+
logging.error(f"取消消费者异常: {str(e)}")
|
|
628
|
+
return False
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
async def stop_consuming(self, timeout: float = 5.0) -> None:
|
|
632
|
+
"""停止消费消息,延长超时时间并增加重试"""
|
|
633
|
+
self._is_consuming = False
|
|
634
|
+
|
|
635
|
+
if self.queue and self._consumer_tag:
|
|
636
|
+
await self._safe_cancel_consumer()
|
|
637
|
+
|
|
638
|
+
# 等待消费任务结束
|
|
639
|
+
if self._consuming_task and not self._consuming_task.done():
|
|
640
|
+
try:
|
|
641
|
+
await asyncio.wait_for(self._consuming_task, timeout=timeout)
|
|
642
|
+
except asyncio.TimeoutError:
|
|
643
|
+
logging.warning(f"等待消费任务结束超时,强制取消")
|
|
644
|
+
self._consuming_task.cancel()
|
|
645
|
+
finally:
|
|
646
|
+
self._consuming_task = None
|
|
647
|
+
|
|
648
|
+
async def _parse_message(self, message: AbstractIncomingMessage) -> Union[Dict[str, Any], str]:
|
|
649
|
+
"""解析消息体,更新活动时间戳"""
|
|
650
|
+
try:
|
|
651
|
+
body_str = message.body.decode('utf-8')
|
|
652
|
+
self._update_activity_timestamp() # 收到消息时更新活动时间
|
|
653
|
+
|
|
654
|
+
if self.auto_parse_json:
|
|
655
|
+
return json.loads(body_str)
|
|
656
|
+
return body_str
|
|
657
|
+
except json.JSONDecodeError:
|
|
658
|
+
logging.warning(f"消息解析JSON失败,返回原始字符串: {body_str}")
|
|
659
|
+
return body_str
|
|
660
|
+
except Exception as e:
|
|
661
|
+
logging.error(f"消息解析出错: {str(e)}")
|
|
662
|
+
return message.body.decode('utf-8')
|
|
663
|
+
|
|
664
|
+
async def _message_wrapper(self, message: AbstractIncomingMessage) -> None:
|
|
665
|
+
if not self.message_handler or not self._is_consuming:
|
|
666
|
+
logging.warning("未设置消息处理器或已停止消费,确认消息")
|
|
667
|
+
# await message.ack()
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
parsed_data = await self._parse_message(message)
|
|
672
|
+
await self.message_handler(MQMsgModel(** parsed_data), message)
|
|
673
|
+
await message.ack()
|
|
674
|
+
self._update_activity_timestamp()
|
|
675
|
+
except Exception as e:
|
|
676
|
+
current_headers = message.headers or {}
|
|
677
|
+
retry_count = current_headers.get('x-retry-count', 0)
|
|
678
|
+
retry_count += 1
|
|
679
|
+
|
|
680
|
+
logging.error(
|
|
681
|
+
f"消息处理出错(第{retry_count}次重试): {str(e)}",
|
|
682
|
+
exc_info=True
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# 判断是否超过最大重试次数
|
|
686
|
+
if retry_count >= MAX_RETRY_COUNT:
|
|
687
|
+
logging.error(
|
|
688
|
+
f"消息已达到最大重试次数({MAX_RETRY_COUNT}次),将被标记为失败不再重试")
|
|
689
|
+
await message.ack()
|
|
690
|
+
self._update_activity_timestamp()
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
# 确保新头信息不为None,基于现有头信息复制(处理首次为None的情况)
|
|
694
|
+
new_headers = current_headers.copy()
|
|
695
|
+
new_headers['x-retry-count'] = retry_count
|
|
696
|
+
|
|
697
|
+
new_message = aio_pika.Message(
|
|
698
|
+
body=message.body,
|
|
699
|
+
content_type=message.content_type,
|
|
700
|
+
headers=new_headers,
|
|
701
|
+
delivery_mode=message.delivery_mode
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# 拒绝原消息(不重新入队)
|
|
705
|
+
await message.reject(requeue=False)
|
|
706
|
+
|
|
707
|
+
# 将新消息重新发布到交换机,实现重试并保留次数记录
|
|
708
|
+
if self.exchange:
|
|
709
|
+
await self.exchange.publish(
|
|
710
|
+
new_message,
|
|
711
|
+
routing_key=self.routing_key or '#'
|
|
712
|
+
)
|
|
713
|
+
self._update_activity_timestamp()
|
|
714
|
+
logging.info(f"消息已重新发布,当前重试次数: {retry_count}")
|
|
715
|
+
|
|
716
|
+
async def __aenter__(self):
|
|
717
|
+
await self.connect()
|
|
718
|
+
return self
|
|
719
|
+
|
|
720
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
721
|
+
await self.close()
|