sycommon-python-lib 0.2.8a12__py3-none-any.whl → 0.2.8a13__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.
@@ -36,103 +36,121 @@ class RabbitMQConnectionMonitor(RabbitMQClientManager):
36
36
  @classmethod
37
37
  async def _monitor_connections(cls):
38
38
  """连接监控任务:定期检查所有客户端连接状态和消费者健康
39
- 信任 aio-pika Robust 自动恢复,Monitor 只做兜底检测+安全重启。"""
39
+ 信任 aio-pika Robust 自动恢复,Monitor 只做兜底检测+安全重启。
40
+
41
+ 🔒 监控自保护(2026-08-24 线上实锤):循环里任何一步 await 卡死
42
+ (broker 半开/网络黑洞/Kafka 日志阻塞)都会让整个监控永久停摆,
43
+ 消费者丢失再无人检测——线上表现为"MQ 相关日志完全消失"。
44
+ 现在每轮整体加 wait_for 看门狗,超时即放弃本轮进下一轮,
45
+ 单轮卡死不再扩散成永久失明。"""
40
46
  logger.info("RabbitMQ连接监控任务启动")
41
47
  while not cls._is_shutdown:
42
48
  try:
43
49
  await asyncio.sleep(cls.RECONNECT_INTERVAL)
50
+ try:
51
+ await asyncio.wait_for(
52
+ cls._monitor_one_round(), timeout=cls.MONITOR_ROUND_TIMEOUT)
53
+ except asyncio.TimeoutError:
54
+ logger.warning(
55
+ f"⚠️ 监控单轮超时({cls.MONITOR_ROUND_TIMEOUT}s),"
56
+ f"疑似 await 卡死,放弃本轮继续(防监控停摆)")
57
+ except Exception as e:
58
+ logger.error("RabbitMQ连接监控任务异常", exc_info=True)
59
+ await asyncio.sleep(cls.RECONNECT_INTERVAL)
60
+
61
+ logger.info("RabbitMQ连接监控任务停止")
44
62
 
45
- # 跳过未初始化的连接池(使用公共方法)
46
- if not cls.is_pool_initialized():
63
+ # 单轮监控超时(秒):池探活+逐 client 检查的总预算
64
+ MONITOR_ROUND_TIMEOUT = 120
65
+
66
+ @classmethod
67
+ async def _monitor_one_round(cls) -> None:
68
+ """单轮监控(从 _monitor_connections 拆出,供看门狗 wait_for 包裹)。"""
69
+ # 跳过未初始化的连接池(使用公共方法)
70
+ if not cls.is_pool_initialized():
71
+ return
72
+
73
+ # 检查连接池本身状态
74
+ pool_alive = await cls._connection_pool.is_alive()
75
+ if not pool_alive:
76
+ # 🔒 池死亡主动重建(2026-08-21):连续 N 轮不活即触发
77
+ # force_reconnect(丢掉旧连接走单飞重建)。不能只等
78
+ # aio-pika 原生恢复——robust 连接存在 transport=None
79
+ # 的卡死中间态(is_closed=False 但已断),原生恢复链
80
+ # 不会推进;纯消费者服务没有发送流量,acquire_channel
81
+ # 永远不会被触发,池就永久死了。
82
+ cls._pool_dead_rounds += 1
83
+ logger.error(
84
+ f"RabbitMQ连接池已断开 "
85
+ f"(连续 {cls._pool_dead_rounds}/"
86
+ f"{cls._POOL_DEAD_REBUILD_THRESHOLD} 轮),等待原生自动重连")
87
+ if cls._pool_dead_rounds >= cls._POOL_DEAD_REBUILD_THRESHOLD:
88
+ cls._pool_dead_rounds = 0
89
+ try:
90
+ asyncio.create_task(
91
+ cls._connection_pool.force_reconnect())
92
+ logger.warning(
93
+ "🔄 连接池死亡超阈值,已触发主动 force_reconnect")
94
+ except Exception as e:
95
+ logger.error(f"触发连接池重建失败: {e}", exc_info=True)
96
+ return
97
+ cls._pool_dead_rounds = 0
98
+
99
+ # 清理已完成的重连任务
100
+ completed_clients = [
101
+ name for name, task in cls._reconnect_tasks.items()
102
+ if task.done()
103
+ ]
104
+ for name in completed_clients:
105
+ del cls._reconnect_tasks[name]
106
+
107
+ # 检查所有客户端连接
108
+ for client_name, client in list(cls._clients.items()):
109
+ try:
110
+ # 跳过正在重连的客户端
111
+ if client_name in cls._reconnect_tasks:
47
112
  continue
48
113
 
49
- # 检查连接池本身状态
50
- pool_alive = await cls._connection_pool.is_alive()
51
- if not pool_alive:
52
- # 🔒 池死亡主动重建(2026-08-21):连续 N 轮不活即触发
53
- # force_reconnect(丢掉旧连接走单飞重建)。不能只等
54
- # aio-pika 原生恢复——robust 连接存在 transport=None
55
- # 的卡死中间态(is_closed=False 但已断),原生恢复链
56
- # 不会推进;纯消费者服务没有发送流量,acquire_channel
57
- # 永远不会被触发,池就永久死了。
58
- cls._pool_dead_rounds += 1
59
- logger.error(
60
- f"RabbitMQ连接池已断开 "
61
- f"(连续 {cls._pool_dead_rounds}/"
62
- f"{cls._POOL_DEAD_REBUILD_THRESHOLD} 轮),等待原生自动重连")
63
- if cls._pool_dead_rounds >= cls._POOL_DEAD_REBUILD_THRESHOLD:
64
- cls._pool_dead_rounds = 0
65
- try:
66
- asyncio.create_task(
67
- cls._connection_pool.force_reconnect())
68
- logger.warning(
69
- "🔄 连接池死亡超阈值,已触发主动 force_reconnect")
70
- except Exception as e:
71
- logger.error(f"触发连接池重建失败: {e}", exc_info=True)
114
+ client_connected = await client.is_connected
115
+
116
+ # 连接断开 → 尝试重连(不做破坏性清理)
117
+ if not client_connected:
118
+ logger.warning(
119
+ f"客户端 '{client_name}' 连接异常,尝试重连")
120
+ task = asyncio.create_task(
121
+ cls._reconnect_client_with_cleanup(client_name, client))
122
+ cls._reconnect_tasks[client_name] = task
72
123
  continue
73
- cls._pool_dead_rounds = 0
74
124
 
75
- # 清理已完成的重连任务
76
- completed_clients = [
77
- name for name, task in cls._reconnect_tasks.items()
78
- if task.done()
79
- ]
80
- for name in completed_clients:
81
- del cls._reconnect_tasks[name]
82
-
83
- # 检查所有客户端连接
84
- for client_name, client in list(cls._clients.items()):
85
- try:
86
- # 跳过正在重连的客户端
87
- if client_name in cls._reconnect_tasks:
88
- continue
89
-
90
- client_connected = await client.is_connected
91
-
92
- # 连接断开 → 尝试重连(不做破坏性清理)
93
- if not client_connected:
94
- logger.warning(
95
- f"客户端 '{client_name}' 连接异常,尝试重连")
96
- task = asyncio.create_task(
97
- cls._reconnect_client_with_cleanup(client_name, client))
98
- cls._reconnect_tasks[client_name] = task
99
- continue
100
-
101
- # 消费者健康检查(本地状态层,快速路径)
102
- if client._message_handler and client._consumer_tag is None:
103
- logger.warning(
104
- f"客户端 '{client_name}' 消费者丢失(handler存在但无consumer_tag),重新注册消费")
105
- await cls._restart_consumer_if_needed(client_name)
106
- continue
107
-
108
- # 消费者健康检查(本地注册表层,broker Cancel 感知)
109
- # broker 主动取消消费者(队列重建/节点切换)会发
110
- # Basic.Cancel,aiormq 收到后从 channel.consumers
111
- # pop 掉 tag——本地 tag 引用还在但注册表已空。
112
- if client._message_handler and client._consumer_tag \
113
- and not client.local_consumer_alive():
114
- logger.warning(
115
- f"客户端 '{client_name}' 本地消费者注册表已失效"
116
- f"(疑似 broker Basic.Cancel),强制重启消费")
117
- if await client.force_restart_consuming():
118
- cls._sync_manager_consumer_tag(client_name, client)
119
- continue
120
-
121
- # 消费者健康检查(broker 真实状态层,兜底路径)
122
- # 根因修复:aio-pika OneShotCallback 恢复链断裂后,本地
123
- # task/tag/handler 全残留(三层全绿),但 broker 侧消费者
124
- # 已消失。唯一可信判据是 broker 的 consumer_count。
125
- if client._message_handler and client.queue_name:
126
- await cls._check_consumer_on_broker(client_name, client)
127
- except Exception as e:
128
- logger.error(
129
- f"监控客户端 '{client_name}' 连接状态失败: {str(e)}", exc_info=True)
125
+ # 消费者健康检查(本地状态层,快速路径)
126
+ if client._message_handler and client._consumer_tag is None:
127
+ logger.warning(
128
+ f"客户端 '{client_name}' 消费者丢失(handler存在但无consumer_tag),重新注册消费")
129
+ await cls._restart_consumer_if_needed(client_name)
130
+ continue
130
131
 
131
- except Exception as e:
132
- logger.error("RabbitMQ连接监控任务异常", exc_info=True)
133
- await asyncio.sleep(cls.RECONNECT_INTERVAL)
132
+ # 消费者健康检查(本地注册表层,broker Cancel 感知)
133
+ # broker 主动取消消费者(队列重建/节点切换)会发
134
+ # Basic.Cancel,aiormq 收到后从 channel.consumers
135
+ # pop 掉 tag——本地 tag 引用还在但注册表已空。
136
+ if client._message_handler and client._consumer_tag \
137
+ and not client.local_consumer_alive():
138
+ logger.warning(
139
+ f"客户端 '{client_name}' 本地消费者注册表已失效"
140
+ f"(疑似 broker Basic.Cancel),强制重启消费")
141
+ if await client.force_restart_consuming():
142
+ cls._sync_manager_consumer_tag(client_name, client)
143
+ continue
134
144
 
135
- logger.info("RabbitMQ连接监控任务停止")
145
+ # 消费者健康检查(broker 真实状态层,兜底路径)
146
+ # 根因修复:aio-pika OneShotCallback 恢复链断裂后,本地
147
+ # task/tag/handler 全残留(三层全绿),但 broker 侧消费者
148
+ # 已消失。唯一可信判据是 broker 的 consumer_count。
149
+ if client._message_handler and client.queue_name:
150
+ await cls._check_consumer_on_broker(client_name, client)
151
+ except Exception as e:
152
+ logger.error(
153
+ f"监控客户端 '{client_name}' 连接状态失败: {str(e)}", exc_info=True)
136
154
 
137
155
  @classmethod
138
156
  async def _restart_consumer_if_needed(cls, client_name: str):
@@ -128,6 +128,49 @@ class RabbitMQConsumerManager(RabbitMQClientManager):
128
128
  name for name in required_clients if name not in running_clients and not cls._is_shutdown]
129
129
  if failed_clients:
130
130
  logger.error(f"以下消费者启动失败: {', '.join(failed_clients)}")
131
+ # 🔒 启动失败兜底重试(2026-08-24 线上实锤):broker 瞬时 reset
132
+ # (Server connection reset 104)会让 setup 阶段的 consume 启动失败,
133
+ # 旧实现只打日志就返回——服务从此裸奔到下次重启(HTTP 正常、MQ 零消费,
134
+ # 监控循环也因 client 未初始化而无事可做)。改为后台持续重试直到成功。
135
+ asyncio.create_task(cls._retry_failed_consumers(failed_clients))
136
+
137
+ @classmethod
138
+ async def _retry_failed_consumers(cls, failed: List[str], max_minutes: int = 30) -> None:
139
+ """对启动失败的消费者做周期重试(指数退避 5s→60s,持续 max_minutes)。
140
+
141
+ 成功标志:task 存活且拿到 consumer_tag。任何一轮部分成功则缩小重试名单。"""
142
+ import time as _time
143
+ pending = list(failed)
144
+ backoff = 5.0
145
+ deadline = _time.time() + max_minutes * 60
146
+ while pending and not cls._is_shutdown and _time.time() < deadline:
147
+ await asyncio.sleep(backoff)
148
+ if cls._is_shutdown:
149
+ return
150
+ still = []
151
+ for name in pending:
152
+ task = cls._consumer_tasks.get(name)
153
+ if (task is not None and not task.done()
154
+ and name in cls._consumer_tags):
155
+ logger.info(f"✅ 消费者 '{name}' 重试后已恢复消费")
156
+ continue
157
+ try:
158
+ logger.warning(
159
+ f"消费者 '{name}' 启动失败重试中(退避{backoff:.0f}s)...")
160
+ await cls.start_consumer(name)
161
+ except Exception as e:
162
+ logger.warning(f"消费者 '{name}' 重试异常: {e}")
163
+ # 重试后再查:可能本轮已成功
164
+ t2 = cls._consumer_tasks.get(name)
165
+ if t2 is not None and not t2.done() and name in cls._consumer_tags:
166
+ logger.info(f"✅ 消费者 '{name}' 重试成功")
167
+ else:
168
+ still.append(name)
169
+ pending = still
170
+ backoff = min(backoff * 2, 60)
171
+ if pending:
172
+ logger.error(
173
+ f"消费者重试超时({max_minutes}min)仍未成功: {pending},需人工介入")
131
174
 
132
175
  @classmethod
133
176
  async def start_all_consumers(cls) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.8a12
3
+ Version: 0.2.8a13
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -264,8 +264,8 @@ sycommon/rabbitmq/rabbitmq_client.py,sha256=G36mcyFdrMSHO5FeMcDaVPEN_d8R2u18GCRH
264
264
  sycommon/rabbitmq/rabbitmq_pool.py,sha256=alZ4vDZAgulSaDbkGJy04nxh239wAVxiYvm2DTLXSBE,18163
265
265
  sycommon/rabbitmq/rabbitmq_service.py,sha256=EBepnotvzZvj34t40HEDup_GAtJI_i1xb795n3nh244,10043
266
266
  sycommon/rabbitmq/rabbitmq_service_client_manager.py,sha256=UZgpD6n0mdo5u3gaGW-jhUuPV50bRNdXFESzL9ABsR4,10623
267
- sycommon/rabbitmq/rabbitmq_service_connection_monitor.py,sha256=EbRmvkNuOSZ_SKHtMFn-TDMlOWuCfhhRybvTOyqX0_o,22685
268
- sycommon/rabbitmq/rabbitmq_service_consumer_manager.py,sha256=GeiS1GEtny5MxopIdKOCfeRB1AzkHZKPXKq9_jwg1KI,10274
267
+ sycommon/rabbitmq/rabbitmq_service_connection_monitor.py,sha256=NtGKkwE_UdSSdFaEjEaLZdrYgUKCgalXrNmOdOVD8cA,23165
268
+ sycommon/rabbitmq/rabbitmq_service_consumer_manager.py,sha256=6lsAq8X6xBAl8WzlVws6NSwkoD0F5P6WIB96upOh8w0,12601
269
269
  sycommon/rabbitmq/rabbitmq_service_core.py,sha256=_t8LQSUB5PyA16uHkjGgh8LvNVYg9DB7ICL0GYD3nlg,10600
270
270
  sycommon/rabbitmq/rabbitmq_service_producer_manager.py,sha256=60xryGoRDRS57cFIVQQj1CPxECXOQRBKXN4OaAknRQA,15132
271
271
  sycommon/sentry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -334,8 +334,8 @@ sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
334
334
  sycommon/tools/user_id.py,sha256=o-zsubGSR5rSSZf8Pk8B7AhE21ahDTTIW5JZmJO6Flw,4761
335
335
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
336
336
  sycommon/xxljob/xxljob_service.py,sha256=1yifwIBNGsCIxLnQjHKiBlbsigc_zvPH-dMTZcNxe-Q,7649
337
- sycommon_python_lib-0.2.8a12.dist-info/METADATA,sha256=AuGxUAW8tC-CIh63rj0YZf0PjwNRnry3-KpD4cyZJFU,8033
338
- sycommon_python_lib-0.2.8a12.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
339
- sycommon_python_lib-0.2.8a12.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
340
- sycommon_python_lib-0.2.8a12.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
341
- sycommon_python_lib-0.2.8a12.dist-info/RECORD,,
337
+ sycommon_python_lib-0.2.8a13.dist-info/METADATA,sha256=5hqjCXuLwT_Xod6rUQqUIuZF1tjiMJqh2akzY0LDCWM,8033
338
+ sycommon_python_lib-0.2.8a13.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
339
+ sycommon_python_lib-0.2.8a13.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
340
+ sycommon_python_lib-0.2.8a13.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
341
+ sycommon_python_lib-0.2.8a13.dist-info/RECORD,,