sycommon-python-lib 0.1.56b5__py3-none-any.whl → 0.1.57b4__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.
Files changed (38) hide show
  1. sycommon/config/Config.py +24 -3
  2. sycommon/config/LangfuseConfig.py +15 -0
  3. sycommon/config/SentryConfig.py +13 -0
  4. sycommon/llm/embedding.py +269 -50
  5. sycommon/llm/get_llm.py +9 -218
  6. sycommon/llm/struct_token.py +192 -0
  7. sycommon/llm/sy_langfuse.py +103 -0
  8. sycommon/llm/usage_token.py +117 -0
  9. sycommon/logging/kafka_log.py +187 -433
  10. sycommon/middleware/exception.py +10 -16
  11. sycommon/middleware/timeout.py +2 -1
  12. sycommon/middleware/traceid.py +81 -76
  13. sycommon/notice/uvicorn_monitor.py +32 -27
  14. sycommon/rabbitmq/rabbitmq_client.py +247 -242
  15. sycommon/rabbitmq/rabbitmq_pool.py +201 -123
  16. sycommon/rabbitmq/rabbitmq_service.py +25 -843
  17. sycommon/rabbitmq/rabbitmq_service_client_manager.py +211 -0
  18. sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +73 -0
  19. sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +285 -0
  20. sycommon/rabbitmq/rabbitmq_service_core.py +117 -0
  21. sycommon/rabbitmq/rabbitmq_service_producer_manager.py +238 -0
  22. sycommon/sentry/__init__.py +0 -0
  23. sycommon/sentry/sy_sentry.py +35 -0
  24. sycommon/services.py +122 -96
  25. sycommon/synacos/nacos_client_base.py +121 -0
  26. sycommon/synacos/nacos_config_manager.py +107 -0
  27. sycommon/synacos/nacos_heartbeat_manager.py +144 -0
  28. sycommon/synacos/nacos_service.py +63 -783
  29. sycommon/synacos/nacos_service_discovery.py +157 -0
  30. sycommon/synacos/nacos_service_registration.py +270 -0
  31. sycommon/tools/env.py +62 -0
  32. sycommon/tools/merge_headers.py +20 -0
  33. sycommon/tools/snowflake.py +101 -153
  34. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/METADATA +10 -8
  35. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/RECORD +38 -20
  36. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/WHEEL +0 -0
  37. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/entry_points.txt +0 -0
  38. {sycommon_python_lib-0.1.56b5.dist-info → sycommon_python_lib-0.1.57b4.dist-info}/top_level.txt +0 -0
@@ -19,12 +19,11 @@ class AsyncProperty:
19
19
  def __get__(self, obj, objtype=None):
20
20
  if obj is None:
21
21
  return self
22
- # 关键:当访问 obj.attr 时,直接返回协程对象,而不是方法本身
23
22
  return self.method(obj)
24
23
 
25
24
 
26
25
  class RabbitMQConnectionPool:
27
- """单连接单通道RabbitMQ客户端 (增强版日志)"""
26
+ """单连接单通道RabbitMQ客户端 (严格执行“先清理后连接”策略)"""
28
27
 
29
28
  def __init__(
30
29
  self,
@@ -33,9 +32,9 @@ class RabbitMQConnectionPool:
33
32
  username: str,
34
33
  password: str,
35
34
  virtualhost: str = "/",
36
- heartbeat: int = 30,
35
+ heartbeat: int = 15,
37
36
  app_name: str = "",
38
- connection_timeout: int = 30,
37
+ connection_timeout: int = 15,
39
38
  reconnect_interval: int = 5,
40
39
  prefetch_count: int = 2,
41
40
  ):
@@ -68,124 +67,231 @@ class RabbitMQConnectionPool:
68
67
 
69
68
  @AsyncProperty
70
69
  async def is_alive(self) -> bool:
71
- """对外暴露的连接存活状态(原子化判断)"""
70
+ """对外暴露的连接存活状态"""
72
71
  async with self._lock:
73
72
  if self._is_shutdown:
74
73
  return False
75
-
76
74
  if not self._initialized:
77
75
  return False
78
-
79
76
  if self._connection is None or self._connection.is_closed:
80
77
  return False
81
-
82
- # 可选:检查主通道是否存活
83
78
  if self._channel is None or self._channel.is_closed:
84
- # 如果你认为通道断了连接也算死,就保留这行;否则删除
85
79
  return False
86
-
87
80
  return True
88
81
 
89
- async def _create_connection_impl(self) -> AbstractRobustConnection:
82
+ async def _cleanup_resources(self):
90
83
  """
91
- 连接创建入口
84
+ 彻底清理旧资源
85
+ 必须在持有 self._lock 的情况下调用
92
86
  """
87
+ logger.info("🧹 [CLEANUP] 开始清理旧资源...")
88
+
89
+ # 1. 清理所有消费者通道
90
+ if self._consumer_channels:
91
+ channels_to_close = list(self._consumer_channels.values())
92
+ self._consumer_channels.clear()
93
+
94
+ for ch in channels_to_close:
95
+ try:
96
+ if not ch.is_closed:
97
+ await ch.close()
98
+ except Exception as e:
99
+ logger.warning(f"⚠️ [CLEANUP_CH] 关闭消费者通道失败: {e}")
100
+
101
+ # 2. 关闭主通道
102
+ if self._channel:
103
+ try:
104
+ if not self._channel.is_closed:
105
+ await self._channel.close()
106
+ except Exception as e:
107
+ logger.warning(f"⚠️ [CLEANUP_MAIN_CH] 关闭主通道失败: {e}")
108
+ finally:
109
+ self._channel = None
110
+
111
+ # 3. 关闭连接
112
+ if self._connection:
113
+ try:
114
+ if not self._connection.is_closed:
115
+ # close() 可能是同步的,也可能是异步的,aio_pika 中通常是异步的
116
+ await self._connection.close()
117
+ except Exception as e:
118
+ logger.warning(f"⚠️ [CLEANUP_CONN] 关闭连接失败: {e}")
119
+ finally:
120
+ self._connection = None
121
+
122
+ logger.info("✅ [CLEANUP] 资源清理完成")
123
+
124
+ async def _create_connection_impl(self, host: str) -> AbstractRobustConnection:
93
125
  conn_url = (
94
- f"amqp://{self.username}:{self.password}@{self._current_host}:{self.port}/"
126
+ f"amqp://{self.username}:{self.password}@{host}:{self.port}/"
95
127
  f"{self.virtualhost}?name={self.app_name}&heartbeat={self.heartbeat}"
96
128
  f"&reconnect_interval={self.reconnect_interval}&fail_fast=1"
97
129
  )
98
- logger.info(
99
- f"🔌 [CONNECT_START] 尝试创建连接 -> {self._current_host}:{self.port}")
130
+ logger.info(f"🔌 [CONNECT] 尝试连接节点: {host}")
100
131
  try:
101
- conn = await connect_robust(conn_url, timeout=self.connection_timeout)
102
- # 注意:connect_robust 返回时,底层 TCP 可能还在握手,但对象已创建
103
- logger.info(f"✅ [CONNECT_OK] 连接对象创建成功: {id(conn)}")
132
+ conn = await asyncio.wait_for(
133
+ connect_robust(conn_url),
134
+ timeout=self.connection_timeout + 5
135
+ )
136
+ logger.info(f"✅ [CONNECT_OK] 节点连接成功: {host}")
104
137
  return conn
105
138
  except Exception as e:
106
- logger.error(f"❌ [CONNECT_FAIL] 连接创建失败: {str(e)}")
107
- raise ConnectionError(f"无法连接RabbitMQ {self._current_host}") from e
108
-
109
- async def _create_channel_impl(self, connection: AbstractRobustConnection) -> RobustChannel:
110
- """创建通道"""
111
- try:
112
- channel = await connection.channel()
113
- await channel.set_qos(prefetch_count=self.prefetch_count)
114
- logger.debug(f"✅ [CHANNEL_OK] 通道创建成功: {id(channel)}")
115
- return channel
116
- except Exception as e:
117
- logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败: {str(e)}")
118
- raise
139
+ logger.error(f"❌ [CONNECT_FAIL] 节点 {host} 连接失败: {str(e)}")
140
+ raise ConnectionError(f"无法连接RabbitMQ {host}") from e
119
141
 
120
142
  async def _ensure_main_channel(self) -> RobustChannel:
121
- """确保主通道有效 (原子操作)"""
143
+ """
144
+ 确保主通道有效
145
+ 逻辑:
146
+ 1. 检查连接状态
147
+ 2. 如果断开 -> 清理 -> 轮询重试
148
+ 3. 如果连接在但通道断开 -> 仅重建通道
149
+ """
122
150
  async with self._lock:
123
151
  if self._is_shutdown:
124
152
  raise RuntimeError("客户端已关闭")
125
153
 
126
- # 1. 确保底层连接存在
127
- conn = self._connection
128
- if conn is None or conn.is_closed:
129
- logger.info("⚠️ [RECONNECT] 检测到连接不存在或已关闭,开始重建...")
130
- conn = await self._create_connection_impl()
131
- self._connection = conn
132
- logger.info(f"🔗 [UPDATE] 连接对象已更新: {id(conn)}")
154
+ # --- 阶段A:连接恢复逻辑 (如果连接断了) ---
155
+ if self._connection is None or self._connection.is_closed:
133
156
 
134
- # 2. 确保主通道存在
157
+ # 1. 【强制】先彻底清理所有旧资源
158
+ await self._cleanup_resources()
159
+
160
+ retry_hosts = self.hosts.copy()
161
+ random.shuffle(retry_hosts)
162
+ last_error = None
163
+ max_attempts = min(len(retry_hosts), 3)
164
+
165
+ # 2. 轮询尝试新连接
166
+ for _ in range(max_attempts):
167
+ if not retry_hosts:
168
+ break
169
+
170
+ host = retry_hosts.pop()
171
+ self._current_host = host
172
+ temp_conn = None
173
+
174
+ try:
175
+ temp_conn = await self._create_connection_impl(host)
176
+
177
+ # 3. 只有在连接成功后,才更新 self._connection
178
+ self._connection = temp_conn
179
+ temp_conn = None # 转移所有权
180
+ self._initialized = True
181
+ last_error = None
182
+ logger.info(f"🔗 [RECONNECT_OK] 切换到节点: {host}")
183
+ break
184
+
185
+ except Exception as e:
186
+ logger.warning(f"⚠️ [RECONNECT_RETRY] 节点 {host} 不可用")
187
+ if temp_conn is not None:
188
+ # 尝试连接失败了,必须把这个“半成品”连接关掉
189
+ try:
190
+ await temp_conn.close()
191
+ except Exception:
192
+ pass
193
+ last_error = e
194
+ await asyncio.sleep(self.reconnect_interval)
195
+
196
+ # 4. 如果所有尝试都失败
197
+ if last_error:
198
+ # 确保状态是干净的
199
+ self._connection = None
200
+ self._initialized = False
201
+ logger.error("💥 [RECONNECT_FATAL] 所有节点重试失败")
202
+ raise ConnectionError("所有 RabbitMQ 节点连接失败") from last_error
203
+
204
+ # --- 阶段B:通道恢复逻辑 (如果连接在但通道断了) ---
205
+ # 注意:这里不需要清理连接,只重置通道
135
206
  if self._channel is None or self._channel.is_closed:
136
- logger.info("⚠️ [RECOVER_CHANNEL] 检测到主通道不存在或已关闭,开始恢复...")
137
- self._channel = await self._create_channel_impl(conn)
207
+ try:
208
+ self._channel = await self._connection.channel()
209
+ await self._channel.set_qos(prefetch_count=self.prefetch_count)
210
+ logger.info(f"✅ [CHANNEL_OK] 主通道已恢复")
211
+ except Exception as e:
212
+ # 如果连通道都创建不了,说明这个连接也是坏的,回滚到阶段A
213
+ logger.error(f"❌ [CHANNEL_FAIL] 通道创建失败,标记连接无效: {e}")
214
+ # 强制清理连接,触发下一次进入阶段A
215
+ await self._cleanup_resources()
216
+ raise
138
217
 
139
218
  return self._channel
140
219
 
141
220
  async def init_pools(self):
142
- """
143
- 初始化入口与异常处理 (修复泄漏的关键)
144
- """
221
+ """初始化入口"""
145
222
  async with self._lock:
146
223
  if self._is_shutdown:
147
224
  raise RuntimeError("客户端已关闭")
148
225
  if self._initialized:
149
226
  return
150
227
 
228
+ # 在 try 之前声明变量,确保 except 块能访问
151
229
  conn_created_in_this_try = None
230
+
152
231
  try:
153
- # 步骤 A: 创建连接 (在锁外进行,避免阻塞其他操作)
154
- conn = await self._create_connection_impl()
155
- conn_created_in_this_try = conn # 记录本次创建的对象,用于失败回滚
232
+ # 锁外创建连接,减少锁持有时间
233
+ init_host = random.choice(self.hosts)
234
+ conn = await self._create_connection_impl(init_host)
235
+
236
+ # 记录本次创建的连接
237
+ conn_created_in_this_try = conn
156
238
 
157
- # 步骤 B: 更新状态和初始化通道 (在锁内进行,保证原子性)
158
239
  async with self._lock:
159
240
  if self._is_shutdown:
160
- # 如果在创建连接期间,外部调用了 close,则必须立即清理刚创建的连接
161
- logger.warning("⚠️ [ABORT] 检测到关闭信号,放弃初始化并清理资源")
162
241
  raise RuntimeError("客户端已关闭")
163
242
 
243
+ # 提交新资源
164
244
  self._connection = conn
165
- self._channel = await self._create_channel_impl(conn)
245
+ self._channel = await self._connection.channel()
246
+ await self._channel.set_qos(prefetch_count=self.prefetch_count)
166
247
  self._initialized = True
167
- logger.info(
168
- f"🚀 [INIT_SUCCESS] 客户端初始化完成. ConnID: {id(self._connection)}")
248
+
249
+ # 所有权转移成功,清空临时引用,防止 finally 重复关闭
250
+ conn_created_in_this_try = None
251
+
252
+ logger.info(f"🚀 [INIT_OK] 连接池初始化完成: {init_host}")
169
253
 
170
254
  except Exception as e:
171
- logger.error(f"💥 [INIT_ERROR] 初始化流程异常: {str(e)}", exc_info=True)
172
- # 如果步骤A成功但步骤B失败(例如通道创建失败),或者步骤B中出错,
173
- # 必须显式关闭在步骤A中创建的连接,否则它会变成“游离连接”。
174
- if conn_created_in_this_try:
175
- logger.warning(
176
- f"🧹 [LEAK_PREVENTION] 检测到初始化失败,正在显式关闭刚创建的连接: {id(conn_created_in_this_try)}")
255
+ logger.error(f"💥 [INIT_FAIL] 初始化异常: {str(e)}")
256
+
257
+ # 这里现在可以合法访问 conn_created_in_this_try
258
+ if conn_created_in_this_try is not None:
177
259
  try:
178
260
  await conn_created_in_this_try.close()
179
- logger.info(
180
- f"✅ [CLOSE_OK] 泄漏连接已关闭: {id(conn_created_in_this_try)}")
181
- except Exception as close_err:
182
- logger.error(f"❌ [CLOSE_ERR] 关闭泄漏连接时出错: {str(close_err)}")
261
+ except Exception:
262
+ pass
183
263
 
184
- # 如果是因为中途关闭导致的错误,不需要再次调用全局 close,否则调用
185
264
  if not self._is_shutdown:
186
265
  await self.close()
187
266
  raise
188
267
 
268
+ async def force_reconnect(self):
269
+ """
270
+ 强制重连
271
+ 严格执行:清理所有资源 -> 尝试建立新资源
272
+ """
273
+ async with self._lock:
274
+ if self._is_shutdown:
275
+ return
276
+
277
+ logger.warning("🔄 [FORCE_RECONNECT] 开始强制重连...")
278
+
279
+ # 1. 【关键】标记未初始化,迫使 _ensure_main_channel 走清理流程
280
+ self._initialized = False
281
+
282
+ # 2. 【关键】立即清理旧资源 (在锁内)
283
+ await self._cleanup_resources()
284
+
285
+ # 此时 self._connection 和 self._channel 均为 None
286
+
287
+ # 3. 锁外触发恢复 (避免阻塞锁太久)
288
+ try:
289
+ await self.acquire_channel()
290
+ logger.info("✅ [FORCE_RECONNECT_OK] 强制重连成功")
291
+ except Exception as e:
292
+ logger.error(f"❌ [FORCE_RECONNECT_FAIL] 强制重连失败: {e}")
293
+ raise
294
+
189
295
  async def acquire_channel(self) -> Tuple[RobustChannel, AbstractRobustConnection]:
190
296
  """获取主通道"""
191
297
  if not self._initialized and not self._is_shutdown:
@@ -193,22 +299,20 @@ class RabbitMQConnectionPool:
193
299
  return await self._ensure_main_channel(), self._connection
194
300
 
195
301
  async def publish_message(self, routing_key: str, message_body: bytes, exchange_name: str = "", **kwargs):
196
- """发布消息"""
197
302
  channel, _ = await self.acquire_channel()
198
303
  try:
199
304
  exchange = channel.default_exchange if not exchange_name else await channel.get_exchange(exchange_name)
200
305
  message = Message(body=message_body, **kwargs)
201
306
  await exchange.publish(message, routing_key=routing_key)
202
- logger.debug(f"📤 [PUBLISH] 消息发布成功 - RK: {routing_key}")
203
307
  except Exception as e:
204
308
  logger.error(f"❌ [PUBLISH_FAIL] 发布失败: {str(e)}")
205
309
  raise
206
310
 
207
311
  async def consume_queue(self, queue_name: str, callback: Callable[[AbstractMessage], asyncio.Future], auto_ack: bool = False, **kwargs):
208
- """消费队列"""
209
312
  if not self._initialized:
210
313
  await self.init_pools()
211
314
 
315
+ # 检查是否已存在
212
316
  async with self._lock:
213
317
  if self._is_shutdown:
214
318
  raise RuntimeError("客户端已关闭")
@@ -218,20 +322,26 @@ class RabbitMQConnectionPool:
218
322
  if not self._connection or self._connection.is_closed:
219
323
  raise RuntimeError("连接不可用,无法启动消费")
220
324
 
325
+ # 声明队列 (使用主通道)
221
326
  await self.declare_queue(queue_name, **kwargs)
222
327
 
223
328
  try:
224
- # 获取原始连接对象创建新通道
225
- conn = self._connection
329
+ # 获取最新连接
330
+ _, conn = await self.acquire_channel()
331
+
332
+ # 创建消费者通道
226
333
  consumer_channel = await conn.channel()
227
334
  await consumer_channel.set_qos(prefetch_count=self.prefetch_count)
228
- logger.info(
229
- f"✅ [CONSUMER_CHANNEL_OK] 消费者通道创建: {id(consumer_channel)}")
230
335
 
231
336
  async with self._lock:
337
+ # 再次检查,防止并发创建
232
338
  if self._is_shutdown:
233
339
  await consumer_channel.close()
234
340
  return
341
+ if queue_name in self._consumer_channels:
342
+ await consumer_channel.close() # 其他协程已经创建了
343
+ return
344
+
235
345
  self._consumer_channels[queue_name] = consumer_channel
236
346
 
237
347
  async def consume_callback_wrapper(message: AbstractMessage):
@@ -240,77 +350,45 @@ class RabbitMQConnectionPool:
240
350
  if not auto_ack:
241
351
  await message.ack()
242
352
  except Exception as e:
243
- logger.error(
244
- f"❌ [CALLBACK_ERR] 消费回调异常 {queue_name}: {str(e)}")
353
+ logger.error(f"❌ [CALLBACK_ERR] {queue_name}: {e}")
245
354
  if not auto_ack:
246
355
  await message.nack(requeue=True)
247
356
 
248
357
  await consumer_channel.basic_consume(
249
- queue_name, consumer_callback=consume_callback_wrapper, auto_ack=auto_ack, **kwargs
358
+ queue_name, consumer_callback=consume_callback_wrapper, auto_ack=auto_ack
250
359
  )
251
- logger.info(f"🎧 [CONSUME_START] 开始消费队列: {queue_name}")
360
+ logger.info(f"🎧 [CONSUME_START] {queue_name}")
252
361
 
253
362
  except Exception as e:
254
- logger.error(f"💥 [CONSUME_ERR] 启动消费失败 {queue_name}: {str(e)}")
363
+ logger.error(f"💥 [CONSUME_ERR] {queue_name}: {e}")
364
+ # 失败时清理字典
255
365
  async with self._lock:
256
366
  if queue_name in self._consumer_channels:
257
- del self._consumer_channels[queue_name]
367
+ # 注意:这里清理的是字典里的引用,通道本身应该在 try 块里被关闭了吗?
368
+ # 如果 consumer_channel 创建成功但 basic_consume 失败,需要手动关闭
369
+ ch = self._consumer_channels.pop(queue_name, None)
370
+ if ch:
371
+ try:
372
+ await ch.close()
373
+ except:
374
+ pass
258
375
  raise
259
376
 
260
377
  async def close(self):
261
- """
262
- 资源销毁入口
263
- """
378
+ """资源销毁"""
264
379
  async with self._lock:
265
380
  if self._is_shutdown:
266
381
  return
267
382
  self._is_shutdown = True
268
383
  self._initialized = False
269
- # 记录即将关闭的连接ID
270
- conn_to_close_id = id(
271
- self._connection) if self._connection else None
272
-
273
- logger.info(f"🛑 [CLOSE_START] 开始关闭客户端... (准备关闭连接: {conn_to_close_id})")
274
-
275
- # 1. 关闭消费者通道
276
- channels_to_close = []
277
- async with self._lock:
278
- channels_to_close = list(self._consumer_channels.values())
279
- self._consumer_channels.clear()
280
-
281
- for ch in channels_to_close:
282
- try:
283
- if not ch.is_closed:
284
- await ch.close()
285
- logger.debug(f"✅ [CLOSE_CHANNEL] 消费者通道已关闭")
286
- except Exception as e:
287
- logger.warning(f"❌ [CLOSE_CHANNEL_ERR] 关闭消费者通道失败: {str(e)}")
288
384
 
289
- # 2. 关闭主通道
290
- if self._channel:
291
- try:
292
- if not self._channel.is_closed:
293
- await self._channel.close()
294
- logger.info(f"✅ [CLOSE_CHANNEL] 主通道已关闭")
295
- except Exception:
296
- pass
297
- self._channel = None
385
+ logger.info("🛑 [CLOSE] 开始关闭连接池...")
298
386
 
299
- # 3. 关闭连接
300
- # 确保在 finally 或显式 close 中调用 connection.close()
301
- if self._connection:
302
- try:
303
- # 打印关闭操作
304
- logger.info(f"🔌 [CLOSE_CONN] 正在关闭连接: {id(self._connection)}")
305
- await self._connection.close()
306
- logger.info(f"✅ [CLOSE_OK] 连接已成功关闭: {id(self._connection)}")
307
- except Exception as e:
308
- logger.warning(f"❌ [CLOSE_ERR] 关闭连接失败: {str(e)}")
309
- self._connection = None
387
+ # 1. 清理所有资源
388
+ await self._cleanup_resources()
310
389
 
311
- logger.info("🏁 [CLOSE_DONE] RabbitMQ客户端已完全关闭")
390
+ logger.info("🏁 [CLOSE] 连接池已关闭")
312
391
 
313
- # --- 辅助方法省略 (declare_queue 等) ---
314
392
  async def declare_queue(self, queue_name: str, **kwargs) -> AbstractQueue:
315
393
  channel, _ = await self.acquire_channel()
316
394
  return await channel.declare_queue(queue_name, **kwargs)