sycommon-python-lib 0.1.10__py3-none-any.whl → 0.1.11__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.

Potentially problematic release.


This version of sycommon-python-lib might be problematic. Click here for more details.

@@ -1,21 +1,35 @@
1
1
  import asyncio
2
2
  import logging
3
- import aio_pika
4
3
  import json
5
- from aio_pika.abc import AbstractIncomingMessage, ExchangeType
6
- from typing import Callable, Coroutine, Optional, Dict, Any, Union
4
+ from typing import Callable, Coroutine, Optional, Dict, Any, Union, Set, List
5
+ from aio_pika import connect_robust, Message, DeliveryMode, ExchangeType
6
+ from aio_pika.abc import (
7
+ AbstractConnection,
8
+ AbstractChannel,
9
+ AbstractExchange,
10
+ AbstractQueue,
11
+ AbstractIncomingMessage,
12
+ ConsumerTag
13
+ )
14
+ from aiormq.exceptions import ChannelInvalidStateError, ConnectionClosed
7
15
 
8
16
  from sycommon.models.mqmsg_model import MQMsgModel
9
- from aiormq.exceptions import ChannelInvalidStateError, ConnectionClosed
10
17
 
11
18
  # 最大重试次数限制
12
19
  MAX_RETRY_COUNT = 3
13
20
 
21
+ logger = logging.getLogger(__name__)
22
+
14
23
 
15
24
  class RabbitMQClient:
25
+ """
26
+ RabbitMQ客户端,支持集群多节点配置,基于aio-pika实现
27
+ 提供自动故障转移、连接恢复和消息可靠性保障
28
+ """
29
+
16
30
  def __init__(
17
31
  self,
18
- host: str,
32
+ hosts: List[str],
19
33
  port: int,
20
34
  username: str,
21
35
  password: str,
@@ -31,61 +45,105 @@ class RabbitMQClient:
31
45
  connection_timeout: int = 10,
32
46
  rpc_timeout: int = 10,
33
47
  app_name: str = "",
34
- reconnection_delay: int = 3,
48
+ reconnection_delay: int = 1,
35
49
  max_reconnection_attempts: int = 5,
36
- heartbeat: int = 30,
37
- keepalive_interval: int = 15
50
+ heartbeat: int = 60,
51
+ prefetch_count: int = 2,
52
+ message_process_timeout: int = 30,
53
+ consumption_stall_threshold: int = 120
38
54
  ):
39
- """初始化RabbitMQ客户端,增加心跳和保活配置"""
40
- self.host = host
55
+ """
56
+ 初始化RabbitMQ客户端,支持集群多节点配置
57
+
58
+ :param hosts: RabbitMQ主机地址列表(集群节点)
59
+ :param port: RabbitMQ端口
60
+ :param username: 用户名
61
+ :param password: 密码
62
+ :param virtualhost: 虚拟主机
63
+ :param exchange_name: 交换机名称
64
+ :param exchange_type: 交换机类型
65
+ :param queue_name: 队列名称
66
+ :param routing_key: 路由键
67
+ :param durable: 是否持久化
68
+ :param auto_delete: 是否自动删除
69
+ :param auto_parse_json: 是否自动解析JSON消息
70
+ :param create_if_not_exists: 如果资源不存在是否创建
71
+ :param connection_timeout: 连接超时时间(秒)
72
+ :param rpc_timeout: RPC操作超时时间(秒)
73
+ :param app_name: 应用名称,用于标识连接
74
+ :param reconnection_delay: 重连延迟(秒)
75
+ :param max_reconnection_attempts: 最大重连尝试次数
76
+ :param heartbeat: 心跳间隔(秒)
77
+ :param prefetch_count: 预取消息数量
78
+ :param message_process_timeout: 消息处理超时时间(秒)
79
+ :param consumption_stall_threshold: 消费停滞检测阈值(秒)
80
+ """
81
+ # 连接参数 - 支持多主机
82
+ self.hosts = [host.strip() for host in hosts if host.strip()]
83
+ if not self.hosts:
84
+ raise ValueError("至少需要提供一个RabbitMQ主机地址")
41
85
  self.port = port
42
86
  self.username = username
43
87
  self.password = password
44
88
  self.virtualhost = virtualhost
89
+ self.app_name = app_name or "rabbitmq-client"
90
+
91
+ # 交换器和队列参数
45
92
  self.exchange_name = exchange_name
46
93
  self.exchange_type = ExchangeType(exchange_type)
47
94
  self.queue_name = queue_name
48
95
  self.routing_key = routing_key
49
96
  self.durable = durable
50
97
  self.auto_delete = auto_delete
98
+
99
+ # 行为控制参数
51
100
  self.auto_parse_json = auto_parse_json
52
101
  self.create_if_not_exists = create_if_not_exists
53
102
  self.connection_timeout = connection_timeout
54
103
  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()
104
+ self.prefetch_count = prefetch_count
61
105
 
62
- # 重连相关配置
106
+ # 重连和保活参数
63
107
  self.reconnection_delay = reconnection_delay
64
108
  self.max_reconnection_attempts = max_reconnection_attempts
109
+ self.heartbeat = heartbeat
65
110
 
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
111
+ # 消息处理参数
112
+ self.message_process_timeout = message_process_timeout
113
+ self.consumption_stall_threshold = consumption_stall_threshold
71
114
 
115
+ # 连接和通道对象
116
+ self.connection: Optional[AbstractConnection] = None
117
+ self.channel: Optional[AbstractChannel] = None
118
+ self.exchange: Optional[AbstractExchange] = None
119
+ self.queue: Optional[AbstractQueue] = None
120
+
121
+ # 当前活跃连接的主机
122
+ self._active_host: Optional[str] = None
123
+
124
+ # 状态跟踪
125
+ self.actual_queue_name: Optional[str] = None
72
126
  self._exchange_exists = False
73
127
  self._queue_exists = False
74
128
  self._queue_bound = False
129
+ self._is_consuming = False
130
+ self._closed = False
131
+ self._consumer_tag: Optional[ConsumerTag] = None
132
+ self._last_activity_timestamp = asyncio.get_event_loop().time()
133
+ self._last_message_processed = asyncio.get_event_loop().time()
75
134
 
76
- # 消息处理器
135
+ # 任务和处理器
77
136
  self.message_handler: Optional[Callable[
78
- [Union[AbstractIncomingMessage, Dict[str, Any]], AbstractIncomingMessage],
79
- Coroutine
137
+ [Union[Dict[str, Any], str], AbstractIncomingMessage],
138
+ Coroutine[Any, Any, None]
80
139
  ]] = None
81
-
82
- # 消费相关
83
- self._consumer_tag: Optional[str] = None
84
140
  self._consuming_task: Optional[asyncio.Task] = None
85
- self._is_consuming: bool = False
86
141
  self._reconnect_task: Optional[asyncio.Task] = None
87
- self._closed: bool = False
88
- self._keepalive_task: Optional[asyncio.Task] = None # 保活任务
142
+ self._keepalive_task: Optional[asyncio.Task] = None
143
+ self._monitor_task: Optional[asyncio.Task] = None
144
+
145
+ # 消息处理跟踪
146
+ self._processing_message_ids: Set[str] = set()
89
147
 
90
148
  @property
91
149
  def is_connected(self) -> bool:
@@ -94,18 +152,24 @@ class RabbitMQClient:
94
152
  self.connection is not None and
95
153
  not self.connection.is_closed and
96
154
  self.channel is not None and
97
- not self.channel.is_closed)
155
+ not self.channel.is_closed and
156
+ self.exchange is not None)
98
157
 
99
- def _update_activity_timestamp(self):
158
+ def _update_activity_timestamp(self) -> None:
100
159
  """更新最后活动时间戳"""
101
- self.last_activity_timestamp = asyncio.get_event_loop().time()
160
+ self._last_activity_timestamp = asyncio.get_event_loop().time()
161
+
162
+ def _update_message_processed_timestamp(self) -> None:
163
+ """更新最后消息处理时间戳"""
164
+ self._last_message_processed = asyncio.get_event_loop().time()
102
165
 
103
166
  async def _check_exchange_exists(self) -> bool:
104
- """检查交换机是否存在,增加超时控制"""
167
+ """检查交换机是否存在"""
105
168
  if not self.channel:
106
169
  return False
107
170
 
108
171
  try:
172
+ # 使用被动模式检查交换机是否存在
109
173
  await asyncio.wait_for(
110
174
  self.channel.declare_exchange(
111
175
  name=self.exchange_name,
@@ -118,18 +182,21 @@ class RabbitMQClient:
118
182
  self._update_activity_timestamp()
119
183
  return True
120
184
  except asyncio.TimeoutError:
121
- logging.error(f"检查交换机 '{self.exchange_name}' 超时")
185
+ logger.error(
186
+ f"检查交换机 '{self.exchange_name}' 超时 (主机: {self._active_host})")
122
187
  return False
123
188
  except Exception as e:
124
- logging.debug(f"交换机 '{self.exchange_name}' 不存在: {str(e)}")
189
+ logger.debug(
190
+ f"交换机 '{self.exchange_name}' 不存在: {str(e)} (主机: {self._active_host})")
125
191
  return False
126
192
 
127
193
  async def _check_queue_exists(self) -> bool:
128
- """检查队列是否存在,增加超时控制"""
194
+ """检查队列是否存在"""
129
195
  if not self.channel or not self.queue_name:
130
196
  return False
131
197
 
132
198
  try:
199
+ # 使用被动模式检查队列是否存在
133
200
  await asyncio.wait_for(
134
201
  self.channel.declare_queue(
135
202
  name=self.queue_name,
@@ -141,21 +208,24 @@ class RabbitMQClient:
141
208
  self._update_activity_timestamp()
142
209
  return True
143
210
  except asyncio.TimeoutError:
144
- logging.error(f"检查队列 '{self.queue_name}' 超时")
211
+ logger.error(
212
+ f"检查队列 '{self.queue_name}' 超时 (主机: {self._active_host})")
145
213
  return False
146
214
  except Exception as e:
147
- logging.debug(f"队列 '{self.queue_name}' 不存在: {str(e)}")
215
+ logger.debug(
216
+ f"队列 '{self.queue_name}' 不存在: {str(e)} (主机: {self._active_host})")
148
217
  return False
149
218
 
150
219
  async def _bind_queue(self) -> bool:
151
- """绑定队列到交换机,增加超时控制和重试"""
220
+ """将队列绑定到交换机"""
152
221
  if not self.channel or not self.queue or not self.exchange:
153
222
  return False
154
223
 
155
- retries = 2 # 绑定操作重试次数
224
+ retries = 2
225
+ bind_routing_key = self.routing_key if self.routing_key else '#'
226
+
156
227
  for attempt in range(retries + 1):
157
228
  try:
158
- bind_routing_key = self.routing_key if self.routing_key else '#'
159
229
  await asyncio.wait_for(
160
230
  self.queue.bind(
161
231
  self.exchange,
@@ -165,319 +235,378 @@ class RabbitMQClient:
165
235
  )
166
236
  self._queue_bound = True
167
237
  self._update_activity_timestamp()
168
- logging.info(
169
- f"队列 '{self.queue_name}' 已绑定到交换机 '{self.exchange_name}',路由键: {bind_routing_key}")
238
+ logger.info(
239
+ f"队列 '{self.queue_name}' 已绑定到交换机 '{self.exchange_name}',路由键: {bind_routing_key} (主机: {self._active_host})")
170
240
  return True
171
241
  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)
242
+ logger.warning(
243
+ f"队列 '{self.queue_name}' 绑定超时(第{attempt+1}次尝试)(主机: {self._active_host})")
178
244
  except Exception as e:
179
- logging.error(f"队列绑定失败(第{attempt+1}次尝试): {str(e)}")
180
- if attempt >= retries:
181
- self._queue_bound = False
182
- return False
245
+ logger.error(
246
+ f"队列绑定失败(第{attempt+1}次尝试): {str(e)} (主机: {self._active_host})")
247
+
248
+ if attempt < retries:
183
249
  await asyncio.sleep(1)
250
+
251
+ self._queue_bound = False
184
252
  return False
185
253
 
254
+ async def _try_connect_host(self, host: str) -> AbstractConnection:
255
+ """尝试连接单个主机"""
256
+ try:
257
+ logger.debug(f"尝试连接主机: {host}:{self.port}")
258
+ return await asyncio.wait_for(
259
+ connect_robust(
260
+ host=host,
261
+ port=self.port,
262
+ login=self.username,
263
+ password=self.password,
264
+ virtualhost=self.virtualhost,
265
+ heartbeat=self.heartbeat,
266
+ client_properties={
267
+ "connection_name": f"{self.app_name}@{host}"
268
+ }
269
+ ),
270
+ timeout=self.connection_timeout
271
+ )
272
+ except Exception as e:
273
+ logger.warning(f"连接主机 {host}:{self.port} 失败: {str(e)}")
274
+ raise
275
+
186
276
  async def connect(self, force_reconnect: bool = False, declare_queue: bool = True) -> None:
187
- """建立连接并检查/创建资源,新增declare_queue参数控制是否声明队列"""
188
- # 增加日志确认参数状态
189
- logging.debug(
190
- f"connect() 调用 - force_reconnect={force_reconnect}, "
191
- f"declare_queue={declare_queue}, create_if_not_exists={self.create_if_not_exists}"
277
+ """
278
+ 建立与RabbitMQ集群的连接(支持多节点故障转移)并初始化所需资源
279
+
280
+ :param force_reconnect: 是否强制重新连接
281
+ :param declare_queue: 是否声明队列
282
+ """
283
+ logger.debug(
284
+ f"连接参数 - force_reconnect={force_reconnect}, "
285
+ f"declare_queue={declare_queue}, create_if_not_exists={self.create_if_not_exists}, "
286
+ f"主机列表: {self.hosts}"
192
287
  )
193
288
 
289
+ # 如果已连接且不强制重连,则直接返回
194
290
  if self.is_connected and not force_reconnect:
195
291
  return
196
292
 
197
- # 如果正在重连,先取消
293
+ # 取消正在进行的重连任务
198
294
  if self._reconnect_task and not self._reconnect_task.done():
199
295
  self._reconnect_task.cancel()
200
296
 
201
- logging.debug(
202
- f"尝试连接RabbitMQ - 主机: {self.host}:{self.port}, "
203
- f"虚拟主机: {self.virtualhost}, "
204
- f"队列: {self.queue_name}, "
205
- f"声明队列: {declare_queue}, "
206
- f"允许创建: {self.create_if_not_exists}"
297
+ logger.debug(
298
+ f"尝试连接RabbitMQ集群 - 主机数量: {len(self.hosts)}, "
299
+ f"虚拟主机: {self.virtualhost}, 队列: {self.queue_name}"
207
300
  )
208
301
 
209
302
  # 重置状态
210
303
  self._exchange_exists = False
211
304
  self._queue_exists = False
212
305
  self._queue_bound = False
306
+ self._active_host = None
213
307
 
214
308
  retries = 0
215
309
  last_exception = None
216
310
 
217
- while retries < 3: # 使用固定重试次数
218
- try:
219
- # 关闭旧连接
220
- if self.connection and not self.connection.is_closed:
221
- await self.connection.close()
222
-
223
- # 建立新连接
224
- self.connection = await asyncio.wait_for(
225
- aio_pika.connect_robust(
226
- host=self.host,
227
- port=self.port,
228
- login=self.username,
229
- password=self.password,
230
- virtualhost=self.virtualhost,
231
- heartbeat=self.heartbeat,
232
- client_properties={
233
- "connection_name": self.app_name or "rabbitmq-client"}
234
- ),
235
- timeout=self.connection_timeout
236
- )
311
+ while retries < self.max_reconnection_attempts:
312
+ # 遍历所有主机尝试连接(故障转移)
313
+ for host in self.hosts:
314
+ try:
315
+ # 关闭现有连接
316
+ if self.connection and not self.connection.is_closed:
317
+ await self.connection.close()
237
318
 
238
- # 创建通道
239
- self.channel = await asyncio.wait_for(
240
- self.connection.channel(),
241
- timeout=self.rpc_timeout
242
- )
243
- await self.channel.set_qos(prefetch_count=2)
319
+ # 尝试连接当前主机
320
+ self.connection = await self._try_connect_host(host)
321
+ self._active_host = host
244
322
 
245
- # 1. 处理交换机
246
- exchange_exists = await self._check_exchange_exists()
247
- if not exchange_exists:
248
- if self.create_if_not_exists:
249
- # 创建交换机
250
- self.exchange = await asyncio.wait_for(
251
- self.channel.declare_exchange(
252
- name=self.exchange_name,
253
- type=self.exchange_type,
254
- durable=self.durable,
255
- auto_delete=self.auto_delete
256
- ),
257
- timeout=self.rpc_timeout
258
- )
259
- self._exchange_exists = True
260
- logging.info(f"已创建交换机 '{self.exchange_name}'")
261
- else:
262
- raise Exception(
263
- f"交换机 '{self.exchange_name}' 不存在且不允许自动创建")
264
- else:
265
- # 获取已有交换机
266
- self.exchange = await asyncio.wait_for(
267
- self.channel.get_exchange(self.exchange_name),
323
+ # 创建通道
324
+ self.channel = await asyncio.wait_for(
325
+ self.connection.channel(),
268
326
  timeout=self.rpc_timeout
269
327
  )
270
- logging.info(f"使用已存在的交换机 '{self.exchange_name}'")
271
-
272
- # 2. 处理队列 - 只有declare_queue为True时才处理
273
- if declare_queue and self.queue_name:
274
- queue_exists = await self._check_queue_exists()
275
328
 
276
- if not queue_exists:
277
- # 关键检查点:确保有权限创建队列
278
- if not self.create_if_not_exists:
329
+ # 设置预取计数,控制消息公平分发
330
+ await self.channel.set_qos(prefetch_count=self.prefetch_count)
331
+
332
+ # 处理交换机
333
+ exchange_exists = await self._check_exchange_exists()
334
+ if not exchange_exists:
335
+ if self.create_if_not_exists:
336
+ # 创建交换机
337
+ self.exchange = await asyncio.wait_for(
338
+ self.channel.declare_exchange(
339
+ name=self.exchange_name,
340
+ type=self.exchange_type,
341
+ durable=self.durable,
342
+ auto_delete=self.auto_delete
343
+ ),
344
+ timeout=self.rpc_timeout
345
+ )
346
+ self._exchange_exists = True
347
+ logger.info(
348
+ f"已创建交换机 '{self.exchange_name}' (主机: {self._active_host})")
349
+ else:
279
350
  raise Exception(
280
- f"队列 '{self.queue_name}' 不存在且不允许自动创建")
281
-
282
- # 创建队列
283
- self.queue = await asyncio.wait_for(
284
- self.channel.declare_queue(
285
- name=self.queue_name,
286
- durable=self.durable,
287
- auto_delete=self.auto_delete,
288
- exclusive=False,
289
- passive=False
290
- ),
291
- timeout=self.rpc_timeout
292
- )
293
- self._queue_exists = True
294
- logging.info(f"已创建队列 '{self.queue_name}'")
351
+ f"交换机 '{self.exchange_name}' 不存在且不允许自动创建 (主机: {self._active_host})")
295
352
  else:
296
- # 获取已有队列
297
- self.queue = await asyncio.wait_for(
298
- self.channel.get_queue(self.queue_name),
353
+ # 获取已有交换机
354
+ self.exchange = await asyncio.wait_for(
355
+ self.channel.get_exchange(self.exchange_name),
299
356
  timeout=self.rpc_timeout
300
357
  )
301
- logging.info(f"使用已存在的队列 '{self.queue_name}'")
302
-
303
- # 3. 绑定队列到交换机
304
- if self.queue and self.exchange:
305
- bound = await self._bind_queue()
306
- if not bound:
358
+ logger.info(
359
+ f"使用已存在的交换机 '{self.exchange_name}' (主机: {self._active_host})")
360
+
361
+ # 处理队列
362
+ if declare_queue and self.queue_name:
363
+ queue_exists = await self._check_queue_exists()
364
+
365
+ if not queue_exists:
366
+ if not self.create_if_not_exists:
367
+ raise Exception(
368
+ f"队列 '{self.queue_name}' 不存在且不允许自动创建 (主机: {self._active_host})")
369
+
370
+ # 创建队列
371
+ self.queue = await asyncio.wait_for(
372
+ self.channel.declare_queue(
373
+ name=self.queue_name,
374
+ durable=self.durable,
375
+ auto_delete=self.auto_delete,
376
+ exclusive=False
377
+ ),
378
+ timeout=self.rpc_timeout
379
+ )
380
+ self._queue_exists = True
381
+ self.actual_queue_name = self.queue_name
382
+ logger.info(
383
+ f"已创建队列 '{self.queue_name}' (主机: {self._active_host})")
384
+ else:
385
+ # 获取已有队列
386
+ self.queue = await asyncio.wait_for(
387
+ self.channel.get_queue(self.queue_name),
388
+ timeout=self.rpc_timeout
389
+ )
390
+ self.actual_queue_name = self.queue_name
391
+ logger.info(
392
+ f"使用已存在的队列 '{self.queue_name}' (主机: {self._active_host})")
393
+
394
+ # 绑定队列到交换机
395
+ if self.queue and self.exchange:
396
+ bound = await self._bind_queue()
397
+ if not bound:
398
+ raise Exception(
399
+ f"队列 '{self.queue_name}' 绑定到交换机 '{self.exchange_name}' 失败 (主机: {self._active_host})")
400
+ else:
307
401
  raise Exception(
308
- f"队列 '{self.queue_name}' 绑定到交换机 '{self.exchange_name}' 失败")
309
- else:
310
- # 不声明队列时,将队列相关状态设为False
311
- self.queue = None
312
- self._queue_exists = False
313
- self._queue_bound = False
314
- logging.debug(f"跳过队列 '{self.queue_name}' 的声明和绑定")
402
+ "队列或交换机未正确初始化 (主机: {self._active_host})")
403
+ else:
404
+ # 不声明队列时的状态处理
405
+ self.queue = None
406
+ self.actual_queue_name = None
407
+ self._queue_exists = False
408
+ self._queue_bound = False
409
+ logger.debug(
410
+ f"跳过队列 '{self.queue_name}' 的声明和绑定 (主机: {self._active_host})")
411
+
412
+ # 验证连接状态
413
+ if not self.is_connected:
414
+ raise Exception(
415
+ f"连接验证失败,状态异常 (主机: {self._active_host})")
315
416
 
316
- # 如果之前在消费,重新开始消费
317
- if self._is_consuming and self.message_handler:
318
- await self.start_consuming()
417
+ # 如果之前在消费,重新开始消费
418
+ if self._is_consuming and self.message_handler:
419
+ await self.start_consuming()
319
420
 
320
- # 启动连接监控和保活任务
321
- self._start_connection_monitor()
322
- self._start_keepalive_task()
421
+ # 启动连接监控和保活任务
422
+ self._start_monitoring()
423
+ self._start_keepalive()
323
424
 
324
- self._update_activity_timestamp()
325
- logging.info(
326
- f"RabbitMQ客户端连接成功 (队列: {self.queue_name}, 声明队列: {declare_queue})")
327
- return
425
+ self._update_activity_timestamp()
426
+ logger.info(
427
+ f"RabbitMQ客户端连接成功 (主机: {self._active_host}, 队列: {self.actual_queue_name})")
428
+ return
328
429
 
329
- except Exception as e:
330
- retries += 1
331
- last_exception = e
332
- logging.warning(
333
- f"连接失败({retries}/3): {str(e)}, create_if_not_exists={self.create_if_not_exists}, 重试中...")
430
+ except Exception as e:
431
+ last_exception = e
432
+ logger.warning(
433
+ f"主机 {host} 连接处理失败: {str(e)},尝试下一个主机...")
434
+ # 清理当前失败的连接资源
435
+ if self.connection and not self.connection.is_closed:
436
+ await self.connection.close()
437
+ self.connection = None
438
+ self.channel = None
439
+ self.exchange = None
440
+ self.queue = None
441
+
442
+ # 所有主机都尝试失败,进行重试
443
+ retries += 1
444
+ logger.warning(
445
+ f"集群连接失败({retries}/{self.max_reconnection_attempts}),所有主机均无法连接,重试中...")
334
446
 
335
- if retries < 3:
447
+ if retries < self.max_reconnection_attempts:
336
448
  await asyncio.sleep(self.reconnection_delay)
337
449
 
338
- logging.error(f"最终连接失败: {str(last_exception)}")
450
+ logger.error(f"最终连接失败: {str(last_exception)}")
339
451
  raise Exception(
340
- f"经过3次重试后仍无法完成连接和资源初始化。最后错误: {str(last_exception)}")
452
+ f"经过{self.max_reconnection_attempts}次重试后仍无法连接到RabbitMQ集群。最后错误: {str(last_exception)}")
341
453
 
342
- def _start_connection_monitor(self):
343
- """启动连接监控任务,检测连接/通道关闭"""
344
- if self._closed:
454
+ def _start_monitoring(self) -> None:
455
+ """启动连接和消费监控任务,支持集群节点故障检测"""
456
+ if self._closed or (self._monitor_task and not self._monitor_task.done()):
345
457
  return
346
458
 
347
- async def monitor_task():
459
+ async def monitor():
348
460
  while not self._closed and self.connection:
349
461
  try:
350
462
  # 检查连接状态
351
463
  if self.connection.is_closed:
352
- logging.warning("检测到RabbitMQ连接已关闭")
464
+ logger.warning(
465
+ f"检测到RabbitMQ连接已关闭 (主机: {self._active_host}),将尝试重连到集群其他节点")
353
466
  await self._schedule_reconnect()
354
467
  return
355
468
 
356
469
  # 检查通道状态
357
470
  if self.channel and self.channel.is_closed:
358
- logging.warning("检测到RabbitMQ通道已关闭")
471
+ logger.warning(
472
+ f"检测到RabbitMQ通道已关闭 (主机: {self._active_host}),将尝试重建")
359
473
  await self._recreate_channel()
360
474
  continue
475
+
476
+ # 检查消费停滞
477
+ if self._is_consuming:
478
+ current_time = asyncio.get_event_loop().time()
479
+ if current_time - self._last_message_processed > self.consumption_stall_threshold:
480
+ logger.warning(
481
+ f"检测到消费停滞超过 {self.consumption_stall_threshold} 秒 (主机: {self._active_host}),将重启消费者")
482
+ if self._is_consuming and self.message_handler:
483
+ await self.stop_consuming()
484
+ await self.start_consuming()
485
+ logger.info("消费者已重启以恢复消费")
361
486
  except Exception as e:
362
- logging.error(f"连接监控任务出错: {str(e)}")
487
+ logger.error(f"监控任务出错: {str(e)}")
363
488
  await asyncio.sleep(1)
364
489
 
365
- await asyncio.sleep(5)
490
+ await asyncio.sleep(5) # 每5秒检查一次
366
491
 
367
- # 创建监控任务
368
- asyncio.create_task(monitor_task())
492
+ self._monitor_task = asyncio.create_task(monitor())
369
493
 
370
- async def _recreate_channel(self):
371
- """重建通道并恢复绑定和消费"""
494
+ async def _recreate_channel(self) -> None:
495
+ """重建通道并恢复绑定和消费,支持当前节点故障时的快速恢复"""
372
496
  try:
497
+ # 连接已关闭时触发完整重连(尝试其他节点)
373
498
  if not self.connection or self.connection.is_closed:
499
+ logger.warning("连接已关闭,触发集群重连")
500
+ await self._schedule_reconnect()
374
501
  return
375
502
 
376
503
  # 重新创建通道
377
504
  self.channel = await self.connection.channel()
378
- await self.channel.set_qos(prefetch_count=2)
505
+ await self.channel.set_qos(prefetch_count=self.prefetch_count)
506
+
507
+ # 重新获取交换机
508
+ self.exchange = await self.channel.get_exchange(self.exchange_name)
379
509
 
380
510
  # 重新绑定队列和交换机
381
- if self.queue and self.exchange:
382
- await self._bind_queue()
511
+ if self.queue_name:
512
+ self.queue = await self.channel.get_queue(self.queue_name)
513
+ if self.queue and self.exchange:
514
+ await self._bind_queue()
383
515
 
384
516
  # 重新开始消费
385
517
  if self._is_consuming and self.message_handler:
386
518
  await self.start_consuming()
387
519
 
388
- logging.info("通道已重新创建并恢复服务")
520
+ logger.info(f"通道已重新创建并恢复服务 (主机: {self._active_host})")
389
521
  self._update_activity_timestamp()
390
522
  except Exception as e:
391
- logging.error(f"重建通道失败: {str(e)}")
523
+ logger.error(f"通道重建失败,触发集群重连: {str(e)} (主机: {self._active_host})")
392
524
  await self._schedule_reconnect()
393
525
 
394
- def _start_keepalive_task(self):
395
- """启动连接保活任务,适配RobustConnection的特性"""
526
+ def _start_keepalive(self) -> None:
527
+ """启动连接保活任务,维护集群连接心跳"""
396
528
  if self._closed or (self._keepalive_task and not self._keepalive_task.done()):
397
529
  return
398
530
 
399
- async def keepalive_task():
531
+ async def keepalive():
400
532
  while not self._closed and self.is_connected:
401
533
  current_time = asyncio.get_event_loop().time()
402
534
  # 检查是否超过指定时间无活动
403
- if current_time - self.last_activity_timestamp > self.heartbeat * 1.5:
404
- logging.debug(f"连接 {self.heartbeat*1.5}s 无活动,执行保活检查")
535
+ if current_time - self._last_activity_timestamp > self.heartbeat * 1.5:
536
+ logger.debug(
537
+ f"连接 {self.heartbeat*1.5}s 无活动,执行保活检查 (主机: {self._active_host})")
405
538
  try:
406
- # 针对RobustConnection的兼容处理
407
- if self.connection:
408
- # 检查连接状态
409
- if self.connection.is_closed:
410
- logging.warning("连接已关闭,触发重连")
411
- await self._schedule_reconnect()
412
- return
413
-
414
- # 尝试一个轻量级操作来保持连接活跃
415
- if self.channel:
416
- # 使用通道声明一个空的交换机(被动模式)作为保活检测
417
- await asyncio.wait_for(
418
- self.channel.declare_exchange(
419
- name=self.exchange_name,
420
- type=self.exchange_type,
421
- passive=True # 被动模式不会创建交换机,仅检查存在性
422
- ),
423
- timeout=5
424
- )
425
-
426
- self._update_activity_timestamp()
539
+ if self.connection and self.connection.is_closed:
540
+ logger.warning("连接已关闭,触发集群重连")
541
+ await self._schedule_reconnect()
542
+ return
543
+
544
+ # 执行轻量级操作保持连接活跃
545
+ if self.channel:
546
+ await asyncio.wait_for(
547
+ self.channel.declare_exchange(
548
+ name=self.exchange_name,
549
+ type=self.exchange_type,
550
+ passive=True # 仅检查存在性
551
+ ),
552
+ timeout=5
553
+ )
554
+
555
+ self._update_activity_timestamp()
427
556
  except asyncio.TimeoutError:
428
- logging.warning("保活检查超时,触发重连")
557
+ logger.warning(
558
+ f"保活检查超时,触发集群重连 (主机: {self._active_host})")
429
559
  await self._schedule_reconnect()
430
560
  except Exception as e:
431
- logging.warning(f"保活检查失败: {str(e)},触发重连")
561
+ logger.warning(
562
+ f"保活检查失败: {str(e)},触发集群重连 (主机: {self._active_host})")
432
563
  await self._schedule_reconnect()
433
564
 
434
- await asyncio.sleep(self.keepalive_interval)
565
+ await asyncio.sleep(self.heartbeat / 2) # 每心跳间隔的一半检查一次
435
566
 
436
- self._keepalive_task = asyncio.create_task(keepalive_task())
567
+ self._keepalive_task = asyncio.create_task(keepalive())
437
568
 
438
- async def _schedule_reconnect(self):
439
- """安排重新连接"""
569
+ async def _schedule_reconnect(self) -> None:
570
+ """安排重新连接(尝试集群中的所有节点)"""
440
571
  if self._reconnect_task and not self._reconnect_task.done():
441
572
  return
442
573
 
443
- logging.info(f"将在 {self.reconnection_delay} 秒后尝试重新连接...")
574
+ logger.info(f"将在 {self.reconnection_delay} 秒后尝试重新连接到RabbitMQ集群...")
444
575
 
445
- async def reconnect_task():
576
+ async def reconnect():
446
577
  try:
447
578
  await asyncio.sleep(self.reconnection_delay)
448
579
  if not self._closed:
449
- await self.connect(force_reconnect=True, max_retries=self.max_reconnection_attempts)
580
+ # 重连时尝试所有节点
581
+ await self.connect(force_reconnect=True)
450
582
  except Exception as e:
451
- logging.error(f"重连任务失败: {str(e)}")
452
- # 如果重连失败,再次安排重连
583
+ logger.error(f"重连任务失败: {str(e)}")
453
584
  if not self._closed:
454
585
  await self._schedule_reconnect()
455
586
 
456
- self._reconnect_task = asyncio.create_task(reconnect_task())
587
+ self._reconnect_task = asyncio.create_task(reconnect())
457
588
 
458
589
  async def close(self) -> None:
459
- """关闭连接,清理所有任务"""
590
+ """关闭连接并清理资源"""
460
591
  self._closed = True
461
592
  self._is_consuming = False
462
593
 
463
- # 取消保活任务
464
- if self._keepalive_task and not self._keepalive_task.done():
465
- self._keepalive_task.cancel()
466
-
467
- # 取消重连任务
468
- if self._reconnect_task and not self._reconnect_task.done():
469
- self._reconnect_task.cancel()
470
-
471
- # 停止消费
472
- if self._consuming_task and not self._consuming_task.done():
473
- self._consuming_task.cancel()
594
+ # 取消所有任务
595
+ for task in [self._keepalive_task, self._reconnect_task,
596
+ self._consuming_task, self._monitor_task]:
597
+ if task and not task.done():
598
+ task.cancel()
599
+ try:
600
+ await task
601
+ except asyncio.CancelledError:
602
+ pass
474
603
 
475
604
  # 关闭连接
476
605
  if self.connection and not self.connection.is_closed:
477
606
  try:
478
607
  await asyncio.wait_for(self.connection.close(), timeout=5)
479
608
  except Exception as e:
480
- logging.warning(f"关闭连接时出错: {str(e)}")
609
+ logger.warning(f"关闭连接时出错 (主机: {self._active_host}): {str(e)}")
481
610
 
482
611
  # 重置状态
483
612
  self.connection = None
@@ -488,241 +617,263 @@ class RabbitMQClient:
488
617
  self._queue_exists = False
489
618
  self._queue_bound = False
490
619
  self._consumer_tag = None
491
- self._consuming_task = None
492
- self._keepalive_task = None
620
+ self._processing_message_ids.clear()
621
+ self._active_host = None
622
+
623
+ logger.info("RabbitMQ客户端已关闭")
493
624
 
494
- async def send_message(
625
+ async def publish(
495
626
  self,
496
627
  message_body: Union[str, Dict[str, Any]],
628
+ routing_key: Optional[str] = None,
497
629
  content_type: str = "application/json",
498
- headers: Optional[Dict[str, Any]] = None
630
+ headers: Optional[Dict[str, Any]] = None,
631
+ delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT
499
632
  ) -> None:
500
- """发送消息到RabbitMQ,带连接检查和重试机制"""
633
+ """
634
+ 发布消息到交换机(自动处理连接故障并重试)
635
+
636
+ :param message_body: 消息体,可以是字符串或字典
637
+ :param routing_key: 路由键,如未指定则使用实例的routing_key
638
+ :param content_type: 内容类型
639
+ :param headers: 消息头
640
+ :param delivery_mode: 投递模式,持久化或非持久化
641
+ """
501
642
  if not self.is_connected:
502
- logging.warning("连接已关闭,尝试重新连接后发送消息")
643
+ logger.warning("连接已关闭,尝试重连后发布消息")
503
644
  await self.connect(force_reconnect=True)
504
645
 
505
646
  if not self.channel or not self.exchange:
506
647
  raise Exception("RabbitMQ连接未初始化")
507
648
 
508
- try:
509
- if isinstance(message_body, dict):
510
- message_body_str = json.dumps(message_body, ensure_ascii=False)
511
- if content_type == "text/plain":
512
- content_type = "application/json"
513
- else:
514
- message_body_str = str(message_body)
515
-
516
- message = aio_pika.Message(
517
- headers=headers,
518
- body=message_body_str.encode(),
519
- content_type=content_type,
520
- delivery_mode=aio_pika.DeliveryMode.PERSISTENT if self.durable else aio_pika.DeliveryMode.TRANSIENT
521
- )
649
+ # 处理消息体
650
+ if isinstance(message_body, dict):
651
+ message_body_str = json.dumps(message_body, ensure_ascii=False)
652
+ if content_type == "text/plain":
653
+ content_type = "application/json"
654
+ else:
655
+ message_body_str = str(message_body)
656
+
657
+ # 创建消息对象
658
+ message = Message(
659
+ body=message_body_str.encode(),
660
+ content_type=content_type,
661
+ headers=headers or {},
662
+ delivery_mode=delivery_mode
663
+ )
522
664
 
523
- await self.exchange.publish(
524
- message,
525
- routing_key=self.routing_key or '#'
526
- )
527
- self._update_activity_timestamp() # 更新活动时间
528
- except (ChannelInvalidStateError, ConnectionClosed) as e:
529
- logging.warning(f"通道/连接已关闭,消息发送失败: {str(e)}")
530
- await self._recreate_channel()
531
- raise
532
- except Exception as e:
533
- logging.warning(f"消息发送失败,尝试重连后再次发送: {str(e)}")
534
- # 尝试重连
535
- await self.connect(force_reconnect=True)
536
- # 重连后再次尝试发送
537
- raise # 让上层处理重发逻辑
665
+ # 发布消息(带重试机制)
666
+ retry_count = 0
667
+ while retry_count < 2: # 最多重试2次
668
+ try:
669
+ await self.exchange.publish(
670
+ message,
671
+ routing_key=routing_key or self.routing_key or '#'
672
+ )
673
+ self._update_activity_timestamp()
674
+ logger.debug(
675
+ f"消息已发布到交换机 '{self.exchange_name}' (主机: {self._active_host})")
676
+ return
677
+ except (ConnectionClosed, ChannelInvalidStateError):
678
+ retry_count += 1
679
+ logger.warning(f"连接已关闭,尝试重连后重新发布 (重试次数: {retry_count})")
680
+ await self.connect(force_reconnect=True)
681
+ except Exception as e:
682
+ retry_count += 1
683
+ logger.error(f"消息发布失败 (重试次数: {retry_count}): {str(e)}")
684
+ if retry_count < 2:
685
+ await asyncio.sleep(1)
686
+
687
+ raise Exception(f"消息发布失败,经过{retry_count}次重试仍未成功")
538
688
 
539
689
  def set_message_handler(
540
690
  self,
541
691
  handler: Callable[
542
- [Union[AbstractIncomingMessage, Dict[str, Any]], AbstractIncomingMessage],
543
- Coroutine
692
+ [Union[Dict[str, Any], str], AbstractIncomingMessage],
693
+ Coroutine[Any, Any, None]
544
694
  ]
545
695
  ) -> None:
546
- """设置消息处理函数"""
547
- self.message_handler = handler
696
+ """
697
+ 设置消息处理函数
548
698
 
549
- async def start_consuming(self, timeout: Optional[float] = None) -> str:
550
- """开始消费消息并返回consumer_tag,支持超时控制和队列检查重试"""
551
- if self._is_consuming:
552
- logging.debug("已经在消费中,返回现有consumer_tag")
553
- return self._consumer_tag
699
+ :param handler: 消息处理函数,接收解析后的消息和原始消息对象
700
+ """
701
+ self.message_handler = handler
554
702
 
555
- # 增加队列检查和连接确保逻辑
556
- max_attempts = 5
557
- attempt = 0
558
- while attempt < max_attempts:
559
- if not self.is_connected:
560
- await self.connect()
703
+ async def start_consuming(self) -> ConsumerTag:
704
+ """
705
+ 开始消费消息
561
706
 
562
- if self.queue:
563
- break
707
+ :return: 消费者标签
708
+ """
709
+ if self._is_consuming:
710
+ logger.debug("已经在消费中,返回现有consumer_tag")
711
+ if self._consumer_tag:
712
+ return self._consumer_tag
713
+ raise Exception("消费已启动但未获取到consumer_tag")
564
714
 
565
- attempt += 1
566
- logging.warning(f"队列尚未初始化,等待后重试({attempt}/{max_attempts})")
567
- await asyncio.sleep(1)
715
+ # 确保连接和队列已准备好
716
+ if not self.is_connected:
717
+ await self.connect()
568
718
 
569
719
  if not self.queue:
570
- # 最后尝试一次显式连接并声明队列
571
- logging.warning("最后尝试重新连接并声明队列")
572
- await self.connect(force_reconnect=True, declare_queue=True)
573
- if not self.queue:
574
- raise Exception("队列未初始化,多次尝试后仍无法创建")
720
+ raise Exception("队列未初始化,无法开始消费")
575
721
 
576
722
  if not self.message_handler:
577
723
  raise Exception("未设置消息处理函数")
578
724
 
579
725
  self._is_consuming = True
726
+ logger.info(
727
+ f"开始消费队列: {self.actual_queue_name} (主机: {self._active_host})")
580
728
 
581
- async def consume_task():
582
- try:
583
- while self._is_consuming and self.is_connected:
584
- try:
585
- # 消费消息
586
- self._consumer_tag = await self.queue.consume(self._message_wrapper)
587
- logging.info(f"消费者已启动,tag: {self._consumer_tag}")
588
-
589
- # 保持消费循环
590
- while self._is_consuming and self.is_connected:
591
- await asyncio.sleep(1)
592
-
593
- # 如果退出循环,取消消费(增加重试逻辑)
594
- if self._consumer_tag and self.queue and not self.queue.channel.is_closed:
595
- await self._safe_cancel_consumer()
729
+ try:
730
+ # 开始消费,使用aio-pika的队列消费方法
731
+ self._consumer_tag = await self.queue.consume(
732
+ self._message_wrapper,
733
+ no_ack=False # 手动确认消息
734
+ )
596
735
 
597
- except (ChannelInvalidStateError, ConnectionClosed) as e:
598
- if self._closed or not self._is_consuming:
599
- break
736
+ logger.info(
737
+ f"消费者已启动,队列: {self.actual_queue_name}, tag: {self._consumer_tag}, 主机: {self._active_host}")
738
+ return self._consumer_tag
739
+ except Exception as e:
740
+ self._is_consuming = False
741
+ logger.error(
742
+ f"启动消费失败: {str(e)} (主机: {self._active_host})", exc_info=True)
743
+ raise
600
744
 
601
- logging.error(f"通道/连接异常: {str(e)},尝试重建通道")
602
- await self._recreate_channel()
603
- await asyncio.sleep(1)
604
- except Exception as e:
605
- if self._closed or not self._is_consuming:
606
- break
745
+ async def _safe_cancel_consumer(self) -> bool:
746
+ """安全取消消费者"""
747
+ if not self._consumer_tag or not self.queue or not self.channel:
748
+ return True
607
749
 
608
- logging.error(f"消费过程中出错: {str(e)}", exc_info=True)
609
- # 如果连接仍然有效,等待后重试
610
- if self.is_connected:
611
- await asyncio.sleep(self.reconnection_delay)
612
- else:
613
- # 连接无效,等待重连
614
- while not self.is_connected and self._is_consuming and not self._closed:
615
- await asyncio.sleep(1)
616
- except asyncio.CancelledError:
617
- logging.info("消费任务已取消")
618
- except Exception as e:
619
- logging.error(f"消费任务出错: {str(e)}", exc_info=True)
620
- finally:
621
- self._is_consuming = False
622
- self._consumer_tag = None
623
- logging.info("消费任务已结束")
624
-
625
- # 保存消费任务引用
626
- self._consuming_task = asyncio.create_task(consume_task())
627
- return self._consumer_tag
628
-
629
- async def _safe_cancel_consumer(self, max_retries: int = 3) -> bool:
630
- """安全取消消费者,增加重试机制"""
631
- if not self._consumer_tag or not self.queue:
750
+ try:
751
+ await asyncio.wait_for(
752
+ self.queue.cancel(self._consumer_tag),
753
+ timeout=self.rpc_timeout
754
+ )
755
+ logger.info(
756
+ f"消费者 {self._consumer_tag} 已取消 (主机: {self._active_host})")
632
757
  return True
758
+ except (ChannelInvalidStateError, ConnectionClosed):
759
+ logger.warning(f"取消消费者失败:通道或连接已关闭 (主机: {self._active_host})")
760
+ return False
761
+ except asyncio.TimeoutError:
762
+ logger.warning(f"取消消费者超时 (主机: {self._active_host})")
763
+ return False
764
+ except Exception as e:
765
+ logger.error(f"取消消费者异常: {str(e)} (主机: {self._active_host})")
766
+ return False
633
767
 
634
- for attempt in range(max_retries):
635
- try:
636
- await asyncio.wait_for(
637
- self.queue.cancel(self._consumer_tag),
638
- timeout=self.rpc_timeout
639
- )
640
- logging.info(f"消费者 {self._consumer_tag} 已取消")
641
- return True
642
- except ChannelInvalidStateError:
643
- if attempt >= max_retries - 1:
644
- logging.error(f"取消消费者 {self._consumer_tag} 失败:通道已关闭")
645
- return False
646
- logging.warning(f"取消消费者尝试 {attempt+1} 失败,通道状态异常,重试中...")
647
- await asyncio.sleep(1)
648
- except asyncio.TimeoutError:
649
- if attempt >= max_retries - 1:
650
- logging.error(f"取消消费者 {self._consumer_tag} 超时")
651
- return False
652
- logging.warning(f"取消消费者尝试 {attempt+1} 超时,重试中...")
653
- await asyncio.sleep(1)
654
- except Exception as e:
655
- logging.error(f"取消消费者异常: {str(e)}")
656
- return False
657
- return False
768
+ async def stop_consuming(self) -> None:
769
+ """停止消费消息,等待正在处理的消息完成"""
770
+ if not self._is_consuming:
771
+ return
658
772
 
659
- async def stop_consuming(self, timeout: float = 5.0) -> None:
660
- """停止消费消息,延长超时时间并增加重试"""
661
773
  self._is_consuming = False
662
774
 
663
- if self.queue and self._consumer_tag:
775
+ # 取消消费者,停止接收新消息
776
+ if self._consumer_tag and self.queue:
664
777
  await self._safe_cancel_consumer()
665
778
 
666
- # 等待消费任务结束
667
- if self._consuming_task and not self._consuming_task.done():
668
- try:
669
- await asyncio.wait_for(self._consuming_task, timeout=timeout)
670
- except asyncio.TimeoutError:
671
- logging.warning(f"等待消费任务结束超时,强制取消")
672
- self._consuming_task.cancel()
673
- finally:
674
- self._consuming_task = None
779
+ # 等待所有正在处理的消息完成
780
+ if self._processing_message_ids:
781
+ logger.info(
782
+ f"等待 {len(self._processing_message_ids)} 个正在处理的消息完成... (主机: {self._active_host})"
783
+ )
784
+ # 循环等待直到所有消息处理完成
785
+ while self._processing_message_ids and not self._closed:
786
+ await asyncio.sleep(0.1)
787
+
788
+ # 清理状态
789
+ self._consumer_tag = None
790
+ self._processing_message_ids.clear()
791
+
792
+ logger.info(
793
+ f"已停止消费队列: {self.actual_queue_name} (主机: {self._active_host})")
675
794
 
676
795
  async def _parse_message(self, message: AbstractIncomingMessage) -> Union[Dict[str, Any], str]:
677
- """解析消息体,更新活动时间戳"""
796
+ """解析消息体"""
678
797
  try:
679
798
  body_str = message.body.decode('utf-8')
680
- self._update_activity_timestamp() # 收到消息时更新活动时间
799
+ self._update_activity_timestamp()
681
800
 
682
801
  if self.auto_parse_json:
683
802
  return json.loads(body_str)
684
803
  return body_str
685
804
  except json.JSONDecodeError:
686
- logging.warning(f"消息解析JSON失败,返回原始字符串: {body_str}")
805
+ logger.warning(f"消息解析JSON失败,返回原始字符串 (主机: {self._active_host})")
687
806
  return body_str
688
807
  except Exception as e:
689
- logging.error(f"消息解析出错: {str(e)}")
808
+ logger.error(f"消息解析出错: {str(e)} (主机: {self._active_host})")
690
809
  return message.body.decode('utf-8')
691
810
 
692
811
  async def _message_wrapper(self, message: AbstractIncomingMessage) -> None:
812
+ """消息处理包装器,处理消息接收、解析、分发和确认"""
693
813
  if not self.message_handler or not self._is_consuming:
694
- logging.warning("未设置消息处理器或已停止消费,确认消息")
695
- # await message.ack()
814
+ logger.warning("未设置消息处理器或已停止消费,确认消息")
815
+ await message.ack()
816
+ return
817
+
818
+ # 跟踪消息ID,防止重复处理
819
+ message_id = message.message_id or str(id(message))
820
+ if message_id in self._processing_message_ids:
821
+ logger.warning(
822
+ f"检测到重复处理的消息ID: {message_id},直接确认 (主机: {self._active_host})")
823
+ await message.ack()
696
824
  return
697
825
 
826
+ self._processing_message_ids.add(message_id)
827
+
698
828
  try:
829
+ logger.debug(
830
+ f"收到队列 {self.actual_queue_name} 的消息: {message_id} (主机: {self._active_host})")
831
+
832
+ # 解析消息
699
833
  parsed_data = await self._parse_message(message)
700
- await self.message_handler(MQMsgModel(** parsed_data), message)
834
+
835
+ # 消息处理超时控制
836
+ # try:
837
+ # await asyncio.wait_for(
838
+ # self.message_handler(MQMsgModel(**parsed_data), message),
839
+ # timeout=self.message_process_timeout
840
+ # )
841
+ # except asyncio.TimeoutError:
842
+ # logger.error(
843
+ # f"消息 {message_id} 处理超时(超过 {self.message_process_timeout} 秒)(主机: {self._active_host})")
844
+ # await message.ack() # 超时也确认,避免无限重试
845
+ # return
846
+ await self.message_handler(MQMsgModel(**parsed_data), message)
847
+
848
+ # 处理成功,确认消息
701
849
  await message.ack()
702
850
  self._update_activity_timestamp()
851
+ self._update_message_processed_timestamp()
852
+ logger.debug(f"消息 {message_id} 处理完成并确认 (主机: {self._active_host})")
853
+
703
854
  except Exception as e:
855
+ # 处理失败,根据重试次数决定是否重新发布
704
856
  current_headers = message.headers or {}
705
857
  retry_count = current_headers.get('x-retry-count', 0)
706
858
  retry_count += 1
707
859
 
708
- logging.error(
709
- f"消息处理出错(第{retry_count}次重试): {str(e)}",
860
+ logger.error(
861
+ f"消息 {message_id} 处理出错(第{retry_count}次重试): {str(e)} (主机: {self._active_host})",
710
862
  exc_info=True
711
863
  )
712
864
 
713
- # 判断是否超过最大重试次数
714
865
  if retry_count >= MAX_RETRY_COUNT:
715
- logging.error(
716
- f"消息已达到最大重试次数({MAX_RETRY_COUNT}次),将被标记为失败不再重试")
866
+ logger.error(
867
+ f"消息 {message_id} 已达到最大重试次数({MAX_RETRY_COUNT}次),标记为失败 (主机: {self._active_host})")
717
868
  await message.ack()
718
869
  self._update_activity_timestamp()
719
870
  return
720
871
 
721
- # 确保新头信息不为None,基于现有头信息复制(处理首次为None的情况)
872
+ # 准备重新发布的消息
722
873
  new_headers = current_headers.copy()
723
874
  new_headers['x-retry-count'] = retry_count
724
875
 
725
- new_message = aio_pika.Message(
876
+ new_message = Message(
726
877
  body=message.body,
727
878
  content_type=message.content_type,
728
879
  headers=new_headers,
@@ -732,14 +883,19 @@ class RabbitMQClient:
732
883
  # 拒绝原消息(不重新入队)
733
884
  await message.reject(requeue=False)
734
885
 
735
- # 将新消息重新发布到交换机,实现重试并保留次数记录
886
+ # 重新发布消息
736
887
  if self.exchange:
737
888
  await self.exchange.publish(
738
889
  new_message,
739
890
  routing_key=self.routing_key or '#'
740
891
  )
741
892
  self._update_activity_timestamp()
742
- logging.info(f"消息已重新发布,当前重试次数: {retry_count}")
893
+ logger.info(
894
+ f"消息 {message_id} 已重新发布,当前重试次数: {retry_count} (主机: {self._active_host})")
895
+ finally:
896
+ # 移除消息ID跟踪
897
+ if message_id in self._processing_message_ids:
898
+ self._processing_message_ids.remove(message_id)
743
899
 
744
900
  async def __aenter__(self):
745
901
  await self.connect()