sycommon-python-lib 0.2.0b0__py3-none-any.whl → 0.2.0b2__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/rabbitmq/rabbitmq_client.py +88 -33
- sycommon/rabbitmq/rabbitmq_pool.py +51 -62
- sycommon/rabbitmq/rabbitmq_service_client_manager.py +51 -12
- {sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/RECORD +8 -8
- {sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/top_level.txt +0 -0
|
@@ -117,11 +117,11 @@ class RabbitMQClient:
|
|
|
117
117
|
logger.info(f"队列重建成功: {self.queue_name}")
|
|
118
118
|
|
|
119
119
|
async def connect(self) -> None:
|
|
120
|
-
"""
|
|
120
|
+
"""连接方法(消费者独立通道 + 移除无效属性检查 + 强制重建队列)"""
|
|
121
121
|
if self._closed:
|
|
122
122
|
raise RuntimeError("客户端已关闭,无法重新连接")
|
|
123
123
|
|
|
124
|
-
# 1. 获取 Condition
|
|
124
|
+
# 1. 获取 Condition 锁,用于管理连接并发和等待
|
|
125
125
|
await self._connect_condition.acquire()
|
|
126
126
|
|
|
127
127
|
try:
|
|
@@ -131,6 +131,7 @@ class RabbitMQClient:
|
|
|
131
131
|
self._connect_condition.release()
|
|
132
132
|
return
|
|
133
133
|
|
|
134
|
+
# 如果已有协程正在连接,等待其完成
|
|
134
135
|
if self._connecting:
|
|
135
136
|
try:
|
|
136
137
|
logger.debug("连接正在进行中,等待现有连接完成...")
|
|
@@ -138,13 +139,14 @@ class RabbitMQClient:
|
|
|
138
139
|
except asyncio.TimeoutError:
|
|
139
140
|
logger.warning("等待前序连接超时,当前协程将尝试强制接管并重连...")
|
|
140
141
|
|
|
141
|
-
#
|
|
142
|
+
# 唤醒后再次检查状态,防止重复连接
|
|
142
143
|
if await self.is_connected:
|
|
143
144
|
if self._connect_condition.locked():
|
|
144
145
|
self._connect_condition.release()
|
|
145
146
|
return
|
|
146
147
|
|
|
147
148
|
# ===== 阶段 B: 标记开始连接并释放锁 =====
|
|
149
|
+
# 释放锁是为了让耗时的连接过程不阻塞其他协程
|
|
148
150
|
self._connecting = True
|
|
149
151
|
self._connect_condition.release()
|
|
150
152
|
|
|
@@ -157,14 +159,15 @@ class RabbitMQClient:
|
|
|
157
159
|
connection_failed = False
|
|
158
160
|
was_consuming = False
|
|
159
161
|
|
|
160
|
-
#
|
|
162
|
+
# 判断当前是否为消费者模式(通过是否有消息处理函数判断)
|
|
163
|
+
is_consumer = self._message_handler is not None
|
|
161
164
|
old_channel = self._channel
|
|
162
165
|
|
|
163
166
|
try:
|
|
164
167
|
# --- 步骤 1: 记录状态并清理旧资源 ---
|
|
165
168
|
was_consuming = self._consumer_tag is not None
|
|
166
169
|
|
|
167
|
-
#
|
|
170
|
+
# 清理旧连接的 close_callbacks,防止重连触发多次
|
|
168
171
|
if self._channel_conn:
|
|
169
172
|
try:
|
|
170
173
|
if self._channel_conn.close_callbacks:
|
|
@@ -172,44 +175,56 @@ class RabbitMQClient:
|
|
|
172
175
|
except Exception:
|
|
173
176
|
pass
|
|
174
177
|
|
|
175
|
-
# 显式关闭旧 Channel
|
|
178
|
+
# 显式关闭旧 Channel
|
|
179
|
+
# 注意:无论是生产者复用的主通道,还是消费者的独立通道,断开时都应显式关闭以释放服务端资源
|
|
176
180
|
if old_channel and not old_channel.is_closed:
|
|
177
181
|
try:
|
|
178
182
|
await old_channel.close()
|
|
179
183
|
except Exception:
|
|
180
184
|
pass
|
|
181
185
|
|
|
182
|
-
#
|
|
186
|
+
# 【修复点】强制重置所有核心资源引用
|
|
187
|
+
# 因为我们即将获取一个新的 Channel,旧的 Exchange 和 Queue 对象(基于旧 Channel)将全部失效。
|
|
188
|
+
# 必须置为 None,强制后续逻辑基于新 Channel 重建这些对象。
|
|
183
189
|
self._channel = None
|
|
184
190
|
self._channel_conn = None
|
|
185
191
|
self._exchange = None
|
|
186
192
|
self._queue = None
|
|
187
193
|
self._consumer_tag = None
|
|
188
194
|
|
|
189
|
-
# --- 步骤 2:
|
|
190
|
-
#
|
|
191
|
-
|
|
195
|
+
# --- 步骤 2: 根据角色获取新连接 ---
|
|
196
|
+
# 生产者:复用连接池的主通道(性能高)
|
|
197
|
+
# 消费者:从连接池获取独立的通道(稳定性高,避免并发冲突)
|
|
198
|
+
if is_consumer:
|
|
199
|
+
logger.debug("获取消费者独立通道...")
|
|
200
|
+
self._channel = await self.connection_pool.acquire_consumer_channel()
|
|
201
|
+
self._channel_conn = self.connection_pool._connection
|
|
202
|
+
else:
|
|
203
|
+
logger.debug("获取生产者主通道...")
|
|
204
|
+
self._channel, self._channel_conn = await self.connection_pool.acquire_channel()
|
|
192
205
|
|
|
193
|
-
# --- 步骤 3:
|
|
206
|
+
# --- 步骤 3: 设置连接关闭回调 ---
|
|
194
207
|
loop = asyncio.get_running_loop()
|
|
195
208
|
|
|
196
209
|
def on_conn_closed(conn, exc):
|
|
197
210
|
if self._closed:
|
|
198
211
|
return
|
|
199
212
|
logger.warning(f"检测到底层连接关闭: {exc}")
|
|
213
|
+
# 确保在循环中安全调用协程
|
|
200
214
|
asyncio.run_coroutine_threadsafe(self._safe_reconnect(), loop)
|
|
201
215
|
|
|
202
216
|
if self._channel_conn:
|
|
203
217
|
self._channel_conn.close_callbacks.add(on_conn_closed)
|
|
204
218
|
|
|
205
219
|
# --- 步骤 4: 重建基础资源 ---
|
|
220
|
+
# 这会在新的 self._channel 上声明 Exchange 和 Queue,并执行绑定
|
|
206
221
|
await self._rebuild_resources()
|
|
207
222
|
|
|
208
223
|
except Exception as e:
|
|
209
224
|
connection_failed = True
|
|
210
225
|
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
211
226
|
|
|
212
|
-
#
|
|
227
|
+
# 发生异常时清理引用
|
|
213
228
|
if self._channel_conn and self._channel_conn.close_callbacks:
|
|
214
229
|
self._channel_conn.close_callbacks.clear()
|
|
215
230
|
|
|
@@ -219,29 +234,59 @@ class RabbitMQClient:
|
|
|
219
234
|
self._queue = None
|
|
220
235
|
self._consumer_tag = None
|
|
221
236
|
|
|
222
|
-
# 不要手动关闭 Pool 返回的连接,只置空引用。
|
|
223
237
|
raise
|
|
224
238
|
|
|
225
239
|
finally:
|
|
226
240
|
# === 阶段 D: 恢复消费与收尾 (重新加锁) ===
|
|
227
|
-
# 确保一定会获取锁
|
|
228
241
|
try:
|
|
229
242
|
await self._connect_condition.acquire()
|
|
230
243
|
except Exception:
|
|
231
244
|
pass
|
|
232
245
|
|
|
233
246
|
try:
|
|
234
|
-
#
|
|
247
|
+
# 只有连接完全成功,且之前处于消费状态,才尝试自动恢复消费
|
|
235
248
|
if not connection_failed and was_consuming and self._message_handler:
|
|
236
249
|
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
250
|
+
|
|
251
|
+
# 【修复核心】
|
|
252
|
+
# 由于在步骤 1 中 self._queue 已被置为 None,
|
|
253
|
+
# 如果 _rebuild_resources 因为某种原因(例如配置条件)没有成功创建队列,
|
|
254
|
+
# 这里需要再次尝试在当前新 Channel 上创建并绑定队列。
|
|
255
|
+
# 不再检查 is_closed(因为该属性不存在),直接检查是否为 None。
|
|
256
|
+
|
|
257
|
+
if self.queue_name and not self._queue:
|
|
258
|
+
try:
|
|
259
|
+
logger.info(f"重连恢复过程中重新声明队列: {self.queue_name}")
|
|
260
|
+
# 在当前新 Channel 上声明队列
|
|
261
|
+
self._queue = await self._channel.declare_queue(
|
|
262
|
+
name=self.queue_name,
|
|
263
|
+
durable=self.durable,
|
|
264
|
+
auto_delete=self.auto_delete,
|
|
265
|
+
passive=not self.create_if_not_exists,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# 【关键步骤】显式绑定队列
|
|
269
|
+
# 即使队列已存在,也必须在新 Channel 上重新绑定,否则服务端路由状态可能不更新
|
|
270
|
+
if self._exchange:
|
|
271
|
+
await self._queue.bind(exchange=self._exchange, routing_key=self.routing_key)
|
|
272
|
+
logger.info(
|
|
273
|
+
f"✅ 重连绑定成功: {self.queue_name} -> {self.routing_key}")
|
|
274
|
+
except Exception as bind_err:
|
|
275
|
+
logger.error(f"❌ 重连恢复队列/绑定失败: {bind_err}")
|
|
276
|
+
# 绑定失败,无法恢复消费
|
|
277
|
+
self._queue = None
|
|
278
|
+
|
|
279
|
+
# 队列对象有效才启动消费
|
|
280
|
+
if self._queue:
|
|
281
|
+
try:
|
|
282
|
+
self._consumer_tag = await self.start_consuming()
|
|
283
|
+
logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
286
|
+
self._consumer_tag = None
|
|
287
|
+
else:
|
|
288
|
+
logger.warning("⚠️ 队列对象无效,无法恢复消费")
|
|
289
|
+
|
|
245
290
|
finally:
|
|
246
291
|
# 最终状态复位
|
|
247
292
|
self._connecting = False
|
|
@@ -418,8 +463,8 @@ class RabbitMQClient:
|
|
|
418
463
|
raise RuntimeError(f"消息发布最终失败: {last_exception}")
|
|
419
464
|
|
|
420
465
|
async def close(self) -> None:
|
|
421
|
-
"""
|
|
422
|
-
# 1.
|
|
466
|
+
"""关闭客户端(支持独立通道的清理与死锁修复)"""
|
|
467
|
+
# 1. 先标记关闭
|
|
423
468
|
self._closed = True
|
|
424
469
|
logger.info("开始关闭RabbitMQ客户端...")
|
|
425
470
|
|
|
@@ -434,17 +479,14 @@ class RabbitMQClient:
|
|
|
434
479
|
# 3. 停止消费
|
|
435
480
|
await self.stop_consuming()
|
|
436
481
|
|
|
437
|
-
# 4.
|
|
438
|
-
# 我们必须获取这个锁,以防止正在进行的 connect() 在我们清理资源时还在操作
|
|
439
|
-
# 但如果 connect 卡在 wait(),我们需要强制唤醒它
|
|
482
|
+
# 4. 处理 _connect_condition 锁
|
|
440
483
|
try:
|
|
441
|
-
# 尝试获取锁,设置超时防止死锁(虽然理论上我们即将 notify_all,但为了保险)
|
|
442
484
|
await asyncio.wait_for(self._connect_condition.acquire(), timeout=2.0)
|
|
443
485
|
except asyncio.TimeoutError:
|
|
444
486
|
logger.warning("获取连接锁超时,强制清理资源...")
|
|
445
487
|
|
|
446
488
|
try:
|
|
447
|
-
#
|
|
489
|
+
# 清理回调
|
|
448
490
|
if self._channel_conn:
|
|
449
491
|
try:
|
|
450
492
|
if self._channel_conn.close_callbacks:
|
|
@@ -452,6 +494,21 @@ class RabbitMQClient:
|
|
|
452
494
|
except Exception:
|
|
453
495
|
pass
|
|
454
496
|
|
|
497
|
+
# 【关键修改】显式关闭持有的通道
|
|
498
|
+
# 无论是生产者(主通道,但 Client 只是持有者,通常不关 Pool 管理的主通道),
|
|
499
|
+
# 还是消费者(独立通道,必须显式关闭),这里都需要处理。
|
|
500
|
+
# 由于我们引入了独立消费者通道,这里必须显式关闭 self._channel
|
|
501
|
+
if self._channel and not self._channel.is_closed:
|
|
502
|
+
try:
|
|
503
|
+
# 注意:如果是主通道,这里关闭可能会影响其他 Producer。
|
|
504
|
+
# 但由于我们的架构中,Consumer 用独立通道,这里大概率是 Consumer 关闭。
|
|
505
|
+
# 为了安全,可以增加判断:如果 shared_channel 标志为 False 才关?
|
|
506
|
+
# 简化策略:统一关闭,因为 Client 被销毁意味着不再需要该通道。
|
|
507
|
+
await self._channel.close()
|
|
508
|
+
logger.debug("客户端通道已关闭")
|
|
509
|
+
except Exception as e:
|
|
510
|
+
logger.warning(f"关闭客户端通道异常: {e}")
|
|
511
|
+
|
|
455
512
|
# 置空资源引用
|
|
456
513
|
self._channel = None
|
|
457
514
|
self._channel_conn = None
|
|
@@ -461,12 +518,10 @@ class RabbitMQClient:
|
|
|
461
518
|
self._conn_close_callback = None
|
|
462
519
|
|
|
463
520
|
finally:
|
|
464
|
-
#
|
|
465
|
-
# 这会让卡在 connect() 阶段 A 的 wait() 的协程醒来,发现 _closed=True 后抛出异常退出
|
|
521
|
+
# 强制重置状态并唤醒所有等待者
|
|
466
522
|
self._connecting = False
|
|
467
523
|
self._connect_condition.notify_all()
|
|
468
524
|
|
|
469
|
-
# 确保锁被释放(如果持有)
|
|
470
525
|
if self._connect_condition.locked():
|
|
471
526
|
self._connect_condition.release()
|
|
472
527
|
|
|
@@ -141,43 +141,38 @@ class RabbitMQConnectionPool:
|
|
|
141
141
|
|
|
142
142
|
async def _ensure_main_channel(self) -> RobustChannel:
|
|
143
143
|
"""
|
|
144
|
-
确保主通道有效 (
|
|
144
|
+
确保主通道有效 (修复:将探活逻辑移入锁内,防止死锁)
|
|
145
145
|
"""
|
|
146
|
-
async with self._lock:
|
|
146
|
+
async with self._lock: # 持有锁贯穿整个方法
|
|
147
147
|
if self._is_shutdown:
|
|
148
148
|
raise RuntimeError("客户端已关闭")
|
|
149
149
|
|
|
150
150
|
# --- 阶段 A: 连接检查与重建 ---
|
|
151
|
-
# 1. 显式检查连接对象状态
|
|
152
151
|
connection_is_dead = False
|
|
153
152
|
if self._connection is None:
|
|
154
153
|
connection_is_dead = True
|
|
155
154
|
else:
|
|
156
155
|
try:
|
|
157
156
|
if self._connection.is_closed:
|
|
158
|
-
logger.info("🔌 连接已关闭,准备重连...")
|
|
159
157
|
connection_is_dead = True
|
|
160
158
|
else:
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# 在 fail_fast=1 模式下,如果底层连接已死,connect() 会迅速抛出异常
|
|
159
|
+
# 【修复】显式探活,保持在锁内
|
|
160
|
+
# 这样如果探活失败,可以直接在锁内进入重建流程,无需重新竞争锁
|
|
164
161
|
try:
|
|
165
|
-
# 设置较短超时,避免阻塞太久
|
|
166
162
|
await asyncio.wait_for(self._connection.connect(timeout=1), timeout=15)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# 捕获 RuntimeError (Connection closed) 或其他异常
|
|
170
|
-
logger.warning(f"⚠️ 连接探活失败 ({e}),判定为死连接,强制重建...")
|
|
163
|
+
except Exception:
|
|
164
|
+
logger.warning("⚠️ 连接探活失败,判定为死连接,强制重建...")
|
|
171
165
|
connection_is_dead = True
|
|
172
|
-
except Exception
|
|
173
|
-
logger.warning(f"⚠️ 检查连接状态异常 ({e}),判定为死连接")
|
|
166
|
+
except Exception:
|
|
174
167
|
connection_is_dead = True
|
|
175
168
|
|
|
176
|
-
#
|
|
169
|
+
# 如果连接死掉,执行清理和重建
|
|
177
170
|
if connection_is_dead:
|
|
178
171
|
await self._cleanup_resources()
|
|
172
|
+
# ... (重建连接逻辑保持不变: 遍历 hosts -> connect_robust -> 赋值 self._connection) ...
|
|
173
|
+
# 确保重建成功,否则抛出异常
|
|
179
174
|
|
|
180
|
-
#
|
|
175
|
+
# 为了代码完整性,这里补全重建逻辑的核心部分(基于你之前的代码)
|
|
181
176
|
retry_hosts = self.hosts.copy()
|
|
182
177
|
random.shuffle(retry_hosts)
|
|
183
178
|
last_error = None
|
|
@@ -186,95 +181,66 @@ class RabbitMQConnectionPool:
|
|
|
186
181
|
for _ in range(max_attempts):
|
|
187
182
|
if not retry_hosts:
|
|
188
183
|
break
|
|
189
|
-
|
|
190
184
|
host = retry_hosts.pop()
|
|
191
185
|
self._current_host = host
|
|
192
186
|
temp_conn = None
|
|
193
|
-
|
|
194
187
|
try:
|
|
195
|
-
# 使用 connect_robust 创建新连接
|
|
196
188
|
conn_url = (
|
|
197
189
|
f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
|
|
198
190
|
f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
|
|
199
191
|
f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
|
|
200
192
|
)
|
|
201
|
-
# 这里必须创建新对象,不能复用旧对象
|
|
202
193
|
temp_conn = await asyncio.wait_for(
|
|
203
194
|
connect_robust(conn_url),
|
|
204
195
|
timeout=self.connection_timeout + 5
|
|
205
196
|
)
|
|
206
|
-
|
|
207
|
-
# 新连接建立成功
|
|
208
197
|
self._connection = temp_conn
|
|
209
|
-
temp_conn = None
|
|
210
198
|
self._initialized = True
|
|
211
199
|
last_error = None
|
|
212
200
|
logger.info(f"✅ [CONNECT_OK] 新连接建立成功: {host}")
|
|
213
201
|
break
|
|
214
|
-
|
|
215
202
|
except Exception as e:
|
|
216
203
|
logger.warning(
|
|
217
204
|
f"⚠️ [RECONNECT_RETRY] 节点 {host} 连接失败: {e}")
|
|
218
|
-
if temp_conn
|
|
205
|
+
if temp_conn:
|
|
219
206
|
try:
|
|
220
207
|
await temp_conn.close()
|
|
221
|
-
except
|
|
208
|
+
except:
|
|
222
209
|
pass
|
|
223
210
|
last_error = e
|
|
224
211
|
await asyncio.sleep(self.reconnect_interval)
|
|
225
212
|
|
|
226
213
|
if last_error:
|
|
227
|
-
self._connection = None
|
|
228
|
-
self._initialized = False
|
|
229
|
-
logger.error("💥 [FATAL] 所有节点连接尝试均失败")
|
|
230
214
|
raise ConnectionError("所有 RabbitMQ 节点连接失败") from last_error
|
|
231
215
|
|
|
232
|
-
# --- 阶段 B: 通道恢复逻辑
|
|
233
|
-
# 此时 self._connection
|
|
216
|
+
# --- 阶段 B: 通道恢复逻辑 ---
|
|
217
|
+
# 此时 self._connection 必须是有效的
|
|
234
218
|
if self._channel is None or self._channel.is_closed:
|
|
235
|
-
|
|
236
|
-
# [修复 3] 使用 while 循环来应对“连接创建瞬间又断开”或“探活漏网”的极端情况
|
|
237
219
|
max_channel_attempts = 2
|
|
238
220
|
for attempt in range(max_channel_attempts):
|
|
239
221
|
try:
|
|
240
|
-
# 尝试创建通道 (这是你报错的地方)
|
|
241
222
|
self._channel = await self._connection.channel()
|
|
242
223
|
await self._channel.set_qos(prefetch_count=self.prefetch_count)
|
|
243
224
|
logger.info(f"✅ [CHANNEL_OK] 主通道已就绪")
|
|
244
|
-
break
|
|
245
|
-
|
|
225
|
+
break
|
|
246
226
|
except Exception as e:
|
|
247
227
|
logger.warning(
|
|
248
228
|
f"⚠️ [CHANNEL_RETRY] 第 {attempt + 1} 次尝试创建通道失败: {e}")
|
|
249
|
-
|
|
250
|
-
# 只有第一次尝试失败时才尝试重建连接,避免无限循环
|
|
251
229
|
if attempt < max_channel_attempts - 1:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# 简化的重建逻辑:使用当前 host 重试
|
|
257
|
-
conn_url = (
|
|
258
|
-
f"amqp://{self.username}:{self.password}@{self._current_host}:{self.port}/"
|
|
259
|
-
f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
|
|
260
|
-
f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
|
|
261
|
-
)
|
|
262
|
-
self._connection = await asyncio.wait_for(
|
|
263
|
-
connect_robust(conn_url),
|
|
264
|
-
timeout=self.connection_timeout + 5
|
|
265
|
-
)
|
|
266
|
-
self._initialized = True
|
|
267
|
-
logger.info("✅ [RECOVER] 连接已重建,将再次尝试创建通道...")
|
|
268
|
-
# continue 进入下一次循环,再次尝试创建 channel
|
|
269
|
-
except Exception as reconnect_err:
|
|
270
|
-
logger.error(
|
|
271
|
-
f"❌ [RECOVER_FAIL] 连接重建失败: {reconnect_err}")
|
|
272
|
-
# 重建失败,抛出原始异常
|
|
273
|
-
raise e
|
|
230
|
+
await self._cleanup_resources() # 通道失败导致连接可能也坏了,重置
|
|
231
|
+
# 简单重试逻辑:这里抛出异常让外层重试,或者在这里递归调用
|
|
232
|
+
# 鉴于复杂度,建议抛出异常
|
|
233
|
+
raise e
|
|
274
234
|
else:
|
|
275
|
-
# 最后一次尝试也失败了,抛出异常
|
|
276
|
-
logger.error("❌ [CHANNEL_FAIL] 经过重试后仍然无法创建通道")
|
|
277
235
|
raise e
|
|
236
|
+
else:
|
|
237
|
+
# 通道存在,进行一次轻量级探活 (保持一致性)
|
|
238
|
+
try:
|
|
239
|
+
await self._channel.set_qos(prefetch_count=self.prefetch_count)
|
|
240
|
+
except Exception:
|
|
241
|
+
logger.warning("⚠️ 通道探活失败,重建通道...")
|
|
242
|
+
self._channel = await self._connection.channel()
|
|
243
|
+
await self._channel.set_qos(prefetch_count=self.prefetch_count)
|
|
278
244
|
|
|
279
245
|
return self._channel
|
|
280
246
|
|
|
@@ -350,6 +316,29 @@ class RabbitMQConnectionPool:
|
|
|
350
316
|
logger.error(f"❌ [FORCE_RECONNECT_FAIL] 强制重连失败: {e}")
|
|
351
317
|
raise
|
|
352
318
|
|
|
319
|
+
async def acquire_consumer_channel(self) -> RobustChannel:
|
|
320
|
+
"""
|
|
321
|
+
专门为消费者获取独立的通道。
|
|
322
|
+
遵循 aio_pika 最佳实践:消费者不应与发布者或其他消费者共享同一个 Channel 对象。
|
|
323
|
+
"""
|
|
324
|
+
# 确保连接池已初始化且连接是活的
|
|
325
|
+
if not self._initialized:
|
|
326
|
+
await self.init_pools()
|
|
327
|
+
|
|
328
|
+
# 确保 self._connection 是有效的(复用 _ensure_main_channel 的连接恢复逻辑)
|
|
329
|
+
await self._ensure_main_channel()
|
|
330
|
+
|
|
331
|
+
# 基于有效连接创建一个新的独立通道
|
|
332
|
+
try:
|
|
333
|
+
# 注意:这里直接使用 self._connection,而不是返回缓存的 self._channel
|
|
334
|
+
consumer_ch = await self._connection.channel()
|
|
335
|
+
await consumer_ch.set_qos(prefetch_count=self.prefetch_count)
|
|
336
|
+
logger.debug("✅ [CONSUMER_CH] 消费者独立通道已创建")
|
|
337
|
+
return consumer_ch
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f"❌ [CONSUMER_CH_FAIL] 创建消费者独立通道失败: {e}")
|
|
340
|
+
raise
|
|
341
|
+
|
|
353
342
|
async def acquire_channel(self) -> Tuple[RobustChannel, AbstractRobustConnection]:
|
|
354
343
|
"""获取主通道"""
|
|
355
344
|
if not self._initialized and not self._is_shutdown:
|
|
@@ -28,47 +28,86 @@ class RabbitMQClientManager(RabbitMQCoreService):
|
|
|
28
28
|
|
|
29
29
|
@classmethod
|
|
30
30
|
async def _clean_client_resources(cls, client: RabbitMQClient) -> None:
|
|
31
|
-
"""
|
|
31
|
+
"""
|
|
32
|
+
清理客户端无效资源(修正版:只清理消费行为,不关闭 Channel)
|
|
33
|
+
|
|
34
|
+
重要变更:
|
|
35
|
+
- 移除了 await client._channel.close() 调用。
|
|
36
|
+
- 原因:关闭 Channel 可能会触发底层连接的 close_callback,导致重连死锁或冲突。
|
|
37
|
+
- Channel 的关闭应该由 RabbitMQClient.connect() 内部的重建逻辑接管。
|
|
38
|
+
"""
|
|
32
39
|
try:
|
|
33
|
-
#
|
|
40
|
+
# 1. 尝试正常停止消费(发送 basic_cancel)
|
|
34
41
|
if client._consumer_tag:
|
|
35
42
|
await client.stop_consuming()
|
|
36
|
-
logger.debug("客户端无效资源清理完成(单通道无需归还)")
|
|
37
43
|
except Exception as e:
|
|
38
|
-
logger.warning(f"
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
logger.warning(f"停止消费逻辑异常: {str(e)}")
|
|
45
|
+
|
|
46
|
+
# 注意:这里不再显式关闭 client._channel
|
|
47
|
+
# 原因:
|
|
48
|
+
# 1. 如果是消费者独立通道,关闭它是安全的,但不如交给 connect() 统一处理(重建前关闭)。
|
|
49
|
+
# 2. 如果是生产者主通道(由 Pool 缓存),绝对不能在这里关闭!否则会影响其他生产者。
|
|
50
|
+
# 3. connect() 方法已经包含了 "显式关闭旧 Channel" 的逻辑,这里重复操作是多余的且危险的。
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# 2. 重置客户端状态引用(为后续 connect() 扫清障碍)
|
|
54
|
+
# 注意:虽然清理了状态,但不要清空 _message_handler!
|
|
41
55
|
client._channel = None
|
|
42
56
|
client._channel_conn = None
|
|
43
57
|
client._exchange = None
|
|
44
58
|
client._queue = None
|
|
45
59
|
client._consumer_tag = None
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning(f"重置客户端状态异常: {str(e)}")
|
|
46
62
|
|
|
47
63
|
@classmethod
|
|
48
64
|
async def _reconnect_client(cls, client_name: str, client: RabbitMQClient) -> bool:
|
|
49
|
-
"""
|
|
65
|
+
"""客户端重连(修正版:保存与恢复 Handler,确保消费可恢复)"""
|
|
50
66
|
if cls._is_shutdown or not (cls._connection_pool and await cls._connection_pool.is_alive):
|
|
51
67
|
return False
|
|
52
68
|
|
|
53
69
|
# 重连冷却
|
|
54
70
|
await asyncio.sleep(cls.RECONNECT_INTERVAL)
|
|
55
71
|
|
|
72
|
+
# 【关键修复 1】状态快照
|
|
73
|
+
# 在清理资源前,保存关键状态,因为 _clean_client_resources 不会清空它,
|
|
74
|
+
# 但为了防御性编程,以及后续逻辑的清晰,我们显式保存。
|
|
75
|
+
saved_handler = client._message_handler
|
|
76
|
+
saved_queue_name = client.queue_name
|
|
77
|
+
|
|
56
78
|
try:
|
|
57
|
-
# 清理旧资源
|
|
79
|
+
# 1. 清理旧资源
|
|
80
|
+
# 修正后的版本:只停止消费,不关闭 Channel
|
|
58
81
|
await cls._clean_client_resources(client)
|
|
59
82
|
|
|
60
|
-
#
|
|
83
|
+
# 2. 【关键修复 2】立即恢复 MessageHandler
|
|
84
|
+
# connect() 方法依赖 self._message_handler 来判断是否执行 "was_consuming" 的恢复逻辑。
|
|
85
|
+
# 如果不恢复,connect() 虽然会连上,但会跳过 start_consuming。
|
|
86
|
+
if saved_handler:
|
|
87
|
+
client._message_handler = saved_handler
|
|
88
|
+
|
|
89
|
+
# 防御性恢复队列名
|
|
90
|
+
if saved_queue_name:
|
|
91
|
+
client.queue_name = saved_queue_name
|
|
92
|
+
|
|
93
|
+
# 3. 执行重连
|
|
94
|
+
# 此时 client 处于“空资源但有 Handler”的状态,正是 connect() 期望的状态
|
|
61
95
|
await client.connect()
|
|
62
96
|
|
|
63
97
|
# 验证重连结果
|
|
64
98
|
if await client.is_connected:
|
|
65
|
-
logger.info(f"客户端 '{client_name}' 重连成功")
|
|
99
|
+
logger.info(f"✅ 客户端 '{client_name}' 重连成功")
|
|
66
100
|
return True
|
|
67
101
|
else:
|
|
68
|
-
logger.warning(f"客户端 '{client_name}' 重连失败:资源未完全初始化")
|
|
102
|
+
logger.warning(f"⚠️ 客户端 '{client_name}' 重连失败:资源未完全初始化")
|
|
69
103
|
return False
|
|
104
|
+
|
|
70
105
|
except Exception as e:
|
|
71
|
-
logger.error(
|
|
106
|
+
logger.error(
|
|
107
|
+
f"❌ 客户端 '{client_name}' 重连失败: {str(e)}", exc_info=True)
|
|
108
|
+
# 异常情况下也尝试恢复 handler,以便下次重试能成功
|
|
109
|
+
if saved_handler:
|
|
110
|
+
client._message_handler = saved_handler
|
|
72
111
|
return False
|
|
73
112
|
|
|
74
113
|
@classmethod
|
|
@@ -51,10 +51,10 @@ sycommon/models/mqsend_config.py,sha256=NQX9dc8PpuquMG36GCVhJe8omAW1KVXXqr6lSRU6
|
|
|
51
51
|
sycommon/models/sso_user.py,sha256=i1WAN6k5sPcPApQEdtjpWDy7VrzWLpOrOQewGLGoGIw,2702
|
|
52
52
|
sycommon/notice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
sycommon/notice/uvicorn_monitor.py,sha256=PrC-OFRE71mL8TcbfdkJRKbjwGAbgsWtyBPWIw1qs08,6753
|
|
54
|
-
sycommon/rabbitmq/rabbitmq_client.py,sha256=
|
|
55
|
-
sycommon/rabbitmq/rabbitmq_pool.py,sha256=
|
|
54
|
+
sycommon/rabbitmq/rabbitmq_client.py,sha256=MC4QNSZpfYSPHY8-iW3RRuZAs0MZcNGWJD9EzCLc68k,22115
|
|
55
|
+
sycommon/rabbitmq/rabbitmq_pool.py,sha256=igQ4Yh96oZyM8UEhNa_rljhZcitHwske3UsjPtBuj8c,18946
|
|
56
56
|
sycommon/rabbitmq/rabbitmq_service.py,sha256=XSHo9HuIJ_lq-vizRh4xJVdZr_2zLqeLhot09qb0euA,2025
|
|
57
|
-
sycommon/rabbitmq/rabbitmq_service_client_manager.py,sha256=
|
|
57
|
+
sycommon/rabbitmq/rabbitmq_service_client_manager.py,sha256=NrJI4JKyItrMwQUFVZu0GWAx8krUdgUeyessFuUWhjo,10280
|
|
58
58
|
sycommon/rabbitmq/rabbitmq_service_connection_monitor.py,sha256=uvoMuJDzJ9i63uVRq1NKFV10CvkbGnTMyEoq2rgjQx8,3013
|
|
59
59
|
sycommon/rabbitmq/rabbitmq_service_consumer_manager.py,sha256=489r1RKd5WrTNMAcWCxUZpt9yWGrNunZlLCCp-M_rzM,11497
|
|
60
60
|
sycommon/rabbitmq/rabbitmq_service_core.py,sha256=6RMvIf78DmEOZmN8dA0duA9oy4ieNswdGrOeyJdD6tU,4753
|
|
@@ -84,8 +84,8 @@ sycommon/tools/merge_headers.py,sha256=u9u8_1ZIuGIminWsw45YJ5qnsx9MB-Fot0VPge7it
|
|
|
84
84
|
sycommon/tools/snowflake.py,sha256=xQlYXwYnI85kSJ1rZ89gMVBhzemP03xrMPVX9vVa3MY,9228
|
|
85
85
|
sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,5612
|
|
86
86
|
sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
|
|
87
|
-
sycommon_python_lib-0.2.
|
|
88
|
-
sycommon_python_lib-0.2.
|
|
89
|
-
sycommon_python_lib-0.2.
|
|
90
|
-
sycommon_python_lib-0.2.
|
|
91
|
-
sycommon_python_lib-0.2.
|
|
87
|
+
sycommon_python_lib-0.2.0b2.dist-info/METADATA,sha256=YydQcYEWv2QGklHoJKu2z1mn8UT-Xtr8V_m0xW0Ig00,7372
|
|
88
|
+
sycommon_python_lib-0.2.0b2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
89
|
+
sycommon_python_lib-0.2.0b2.dist-info/entry_points.txt,sha256=q_h2nbvhhmdnsOUZEIwpuoDjaNfBF9XqppDEmQn9d_A,46
|
|
90
|
+
sycommon_python_lib-0.2.0b2.dist-info/top_level.txt,sha256=98CJ-cyM2WIKxLz-Pf0AitWLhJyrfXvyY8slwjTXNuc,17
|
|
91
|
+
sycommon_python_lib-0.2.0b2.dist-info/RECORD,,
|
|
File without changes
|
{sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{sycommon_python_lib-0.2.0b0.dist-info → sycommon_python_lib-0.2.0b2.dist-info}/top_level.txt
RENAMED
|
File without changes
|