sycommon-python-lib 0.1.59__tar.gz → 0.2.0b1__tar.gz
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_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/PKG-INFO +2 -1
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/pyproject.toml +2 -1
- sycommon_python_lib-0.2.0b1/src/sycommon/notice/uvicorn_monitor.py +188 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_client.py +157 -46
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_pool.py +114 -64
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_service_client_manager.py +20 -5
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/nacos_service_discovery.py +9 -6
- sycommon_python_lib-0.2.0b1/src/sycommon/tests/test_email.py +172 -0
- sycommon_python_lib-0.2.0b1/src/sycommon/tools/syemail.py +173 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon_python_lib.egg-info/PKG-INFO +2 -1
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon_python_lib.egg-info/SOURCES.txt +2 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon_python_lib.egg-info/requires.txt +1 -0
- sycommon_python_lib-0.1.59/src/sycommon/notice/uvicorn_monitor.py +0 -200
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/README.md +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/setup.cfg +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/command/cli.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/Config.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/DatabaseConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/EmbeddingConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/LLMConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/LangfuseConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/MQConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/RerankerConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/SentryConfig.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/config/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/database/async_base_db_service.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/database/async_database_service.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/database/base_db_service.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/database/database_service.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/health/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/health/health_check.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/health/metrics.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/health/ping.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/embedding.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/get_llm.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/llm_logger.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/llm_tokens.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/struct_token.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/sy_langfuse.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/llm/usage_token.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/logging/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/logging/async_sql_logger.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/logging/kafka_log.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/logging/logger_levels.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/logging/logger_wrapper.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/logging/sql_logger.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/context.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/cors.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/docs.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/exception.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/middleware.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/monitor_memory.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/mq.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/timeout.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/middleware/traceid.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/base_http.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/log.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/mqlistener_config.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/mqmsg_model.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/mqsend_config.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/models/sso_user.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/notice/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_service.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_service_core.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_service_producer_manager.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/sentry/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/sentry/sy_sentry.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/services.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/sse/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/sse/event.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/sse/sse.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/example.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/example2.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/feign.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/feign_client.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/nacos_client_base.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/nacos_config_manager.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/nacos_heartbeat_manager.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/nacos_service.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/nacos_service_registration.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/synacos/param.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/tools/__init__.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/tools/docs.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/tools/env.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/tools/merge_headers.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/tools/snowflake.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/tools/timing.py +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sycommon-python-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0b1
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -21,6 +21,7 @@ Requires-Dist: nacos-sdk-python<3.0,>=2.0.9
|
|
|
21
21
|
Requires-Dist: psutil>=7.2.1
|
|
22
22
|
Requires-Dist: pydantic>=2.12.5
|
|
23
23
|
Requires-Dist: python-dotenv>=1.2.1
|
|
24
|
+
Requires-Dist: python-multipart>=0.0.21
|
|
24
25
|
Requires-Dist: pyyaml>=6.0.3
|
|
25
26
|
Requires-Dist: sentry-sdk[fastapi]>=2.49.0
|
|
26
27
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.45
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sycommon-python-lib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0b1"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -22,6 +22,7 @@ dependencies = [
|
|
|
22
22
|
"psutil>=7.2.1",
|
|
23
23
|
"pydantic>=2.12.5",
|
|
24
24
|
"python-dotenv>=1.2.1",
|
|
25
|
+
"python-multipart>=0.0.21",
|
|
25
26
|
"pyyaml>=6.0.3",
|
|
26
27
|
"sentry-sdk[fastapi]>=2.49.0",
|
|
27
28
|
"sqlalchemy[asyncio]>=2.0.45",
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
import aiohttp
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
8
|
+
from sycommon.config.Config import Config
|
|
9
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def send_wechat_markdown_msg(
|
|
13
|
+
content: str,
|
|
14
|
+
webhook: str = None
|
|
15
|
+
) -> Optional[dict]:
|
|
16
|
+
"""
|
|
17
|
+
异步发送企业微信Markdown格式的WebHook消息
|
|
18
|
+
"""
|
|
19
|
+
# 构造请求体(Markdown格式)
|
|
20
|
+
payload = {
|
|
21
|
+
"msgtype": "markdown",
|
|
22
|
+
"markdown": {
|
|
23
|
+
"content": content
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# 使用 json 参数自动处理序列化和 Content-Type
|
|
29
|
+
async with aiohttp.ClientSession() as session:
|
|
30
|
+
async with session.post(
|
|
31
|
+
url=webhook,
|
|
32
|
+
json=payload,
|
|
33
|
+
timeout=aiohttp.ClientTimeout(total=10)
|
|
34
|
+
) as response:
|
|
35
|
+
response_data = await response.json()
|
|
36
|
+
|
|
37
|
+
if response.status == 200 and response_data.get("errcode") == 0:
|
|
38
|
+
SYLogger.info(f"消息发送成功: {response_data}")
|
|
39
|
+
return response_data
|
|
40
|
+
else:
|
|
41
|
+
SYLogger.warning(
|
|
42
|
+
f"消息发送失败 - 状态码: {response.status}, 响应: {response_data}")
|
|
43
|
+
return None
|
|
44
|
+
except Exception as e:
|
|
45
|
+
SYLogger.error(f"发送企业微信消息异常: {str(e)}")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def send_webhook(error_info: dict = None, webhook: str = None):
|
|
50
|
+
"""
|
|
51
|
+
发送服务启动结果的企业微信通知
|
|
52
|
+
"""
|
|
53
|
+
# 获取服务名和环境(增加默认值保护)
|
|
54
|
+
try:
|
|
55
|
+
config = Config().config
|
|
56
|
+
service_name = config.get('Name', "未知服务")
|
|
57
|
+
env = config.get('Nacos', {}).get('namespaceId', '未知环境')
|
|
58
|
+
# 注意:这里使用了大写开头的 WebHook,请确保配置文件中键名一致
|
|
59
|
+
webHook = config.get('llm', {}).get('WebHook')
|
|
60
|
+
except Exception as e:
|
|
61
|
+
service_name = "未知服务"
|
|
62
|
+
env = "未知环境"
|
|
63
|
+
webHook = None
|
|
64
|
+
SYLogger.warning(f"读取配置失败: {str(e)}")
|
|
65
|
+
|
|
66
|
+
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
67
|
+
|
|
68
|
+
# 如果没有错误信息,就不发送失败告警
|
|
69
|
+
if not error_info:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# 启动失败的通知内容(包含详细错误信息)
|
|
73
|
+
error_type = error_info.get("error_type", "未知错误")
|
|
74
|
+
error_msg = error_info.get("error_msg", "无错误信息")
|
|
75
|
+
stack_trace = error_info.get("stack_trace", "无堆栈信息")[:1000] # 适当增加长度限制
|
|
76
|
+
elapsed_time = error_info.get("elapsed_time", 0)
|
|
77
|
+
|
|
78
|
+
markdown_content = f"""### {service_name}服务启动失败告警 ⚠️
|
|
79
|
+
> 环境: <font color="warning">{env}</font>
|
|
80
|
+
> 启动时间: <font color="comment">{start_time}</font>
|
|
81
|
+
> 耗时: <font color="comment">{elapsed_time:.2f}秒</font>
|
|
82
|
+
> 错误类型: <font color="danger">{error_type}</font>
|
|
83
|
+
> 错误信息: <font color="danger">{error_msg}</font>
|
|
84
|
+
> 错误堆栈: {stack_trace}"""
|
|
85
|
+
|
|
86
|
+
if webhook or webHook:
|
|
87
|
+
result = await send_wechat_markdown_msg(
|
|
88
|
+
content=markdown_content,
|
|
89
|
+
webhook=webhook or webHook
|
|
90
|
+
)
|
|
91
|
+
SYLogger.info(f"通知发送结果: {result}")
|
|
92
|
+
else:
|
|
93
|
+
SYLogger.info("未设置企业微信WebHook,跳过告警发送")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run(*args, webhook: str = None, **kwargs):
|
|
97
|
+
"""
|
|
98
|
+
带企业微信告警的Uvicorn启动监控
|
|
99
|
+
"""
|
|
100
|
+
# 判断环境
|
|
101
|
+
try:
|
|
102
|
+
env = Config().config.get('Nacos', {}).get('namespaceId', 'dev')
|
|
103
|
+
except:
|
|
104
|
+
env = 'dev'
|
|
105
|
+
|
|
106
|
+
# 如果是生产环境,通常不需要这种启动脚本进行告警干扰,或者你需要根据实际情况调整
|
|
107
|
+
if env == "prod":
|
|
108
|
+
import uvicorn
|
|
109
|
+
uvicorn.run(*args, **kwargs)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# 记录启动开始时间
|
|
113
|
+
start_time = datetime.now()
|
|
114
|
+
|
|
115
|
+
if webhook:
|
|
116
|
+
# 脱敏展示webhook
|
|
117
|
+
try:
|
|
118
|
+
parsed = urlparse(webhook)
|
|
119
|
+
query = parse_qs(parsed.query)
|
|
120
|
+
if 'key' in query and query['key'][0]:
|
|
121
|
+
key = query['key'][0]
|
|
122
|
+
masked_key = key[:8] + "****" if len(key) > 8 else key + "****"
|
|
123
|
+
query['key'] = [masked_key]
|
|
124
|
+
masked_query = urlencode(query, doseq=True)
|
|
125
|
+
masked_webhook = urlunparse(
|
|
126
|
+
(parsed.scheme, parsed.netloc, parsed.path, parsed.params, masked_query, parsed.fragment))
|
|
127
|
+
SYLogger.info(f"自定义企业微信WebHook: {masked_webhook}")
|
|
128
|
+
except Exception:
|
|
129
|
+
SYLogger.info("自定义Webhook格式解析失败,但会尝试使用")
|
|
130
|
+
|
|
131
|
+
# 初始化错误信息
|
|
132
|
+
error_info = None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
import uvicorn
|
|
136
|
+
# 执行启动
|
|
137
|
+
uvicorn.run(*args, **kwargs)
|
|
138
|
+
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
# 处理用户手动中断
|
|
141
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
142
|
+
SYLogger.info(f"\n{'='*50}")
|
|
143
|
+
SYLogger.info(f"ℹ️ 应用被用户手动中断")
|
|
144
|
+
SYLogger.info(f"启动耗时: {elapsed:.2f} 秒")
|
|
145
|
+
SYLogger.info(f"{'='*50}\n")
|
|
146
|
+
# 手动中断不需要错误告警,也不需要 sys.exit(1)
|
|
147
|
+
sys.exit(0)
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
# 捕获启动失败异常
|
|
151
|
+
elapsed = (datetime.now() - start_time).total_seconds()
|
|
152
|
+
stack_trace = traceback.format_exc()
|
|
153
|
+
|
|
154
|
+
error_info = {
|
|
155
|
+
"error_type": type(e).__name__,
|
|
156
|
+
"error_msg": str(e),
|
|
157
|
+
"stack_trace": stack_trace,
|
|
158
|
+
"elapsed_time": elapsed
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# 打印详细错误
|
|
162
|
+
SYLogger.error(f"\n{'='*50}")
|
|
163
|
+
SYLogger.error(f"🚨 应用启动失败!")
|
|
164
|
+
SYLogger.error(f"失败时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
165
|
+
SYLogger.error(f"错误类型: {type(e).__name__}")
|
|
166
|
+
SYLogger.error(f"错误信息: {str(e)}")
|
|
167
|
+
SYLogger.error(f"\n📝 错误堆栈:")
|
|
168
|
+
SYLogger.error(f"-"*50)
|
|
169
|
+
traceback.print_exc(file=sys.stdout)
|
|
170
|
+
SYLogger.error(f"\n⏱️ 启动耗时: {elapsed:.2f} 秒")
|
|
171
|
+
SYLogger.error(f"{'='*50}\n")
|
|
172
|
+
|
|
173
|
+
finally:
|
|
174
|
+
# 创建新的事件循环来发送通知,防止与 Uvicorn 的循环冲突
|
|
175
|
+
if error_info:
|
|
176
|
+
try:
|
|
177
|
+
loop = asyncio.new_event_loop()
|
|
178
|
+
asyncio.set_event_loop(loop)
|
|
179
|
+
loop.run_until_complete(send_webhook(
|
|
180
|
+
error_info=error_info,
|
|
181
|
+
webhook=webhook
|
|
182
|
+
))
|
|
183
|
+
loop.close()
|
|
184
|
+
except Exception as e:
|
|
185
|
+
SYLogger.error(f"错误:异步通知发送失败 - {str(e)}")
|
|
186
|
+
|
|
187
|
+
# 只有确实有错误时才以状态码 1 退出
|
|
188
|
+
sys.exit(1)
|
{sycommon_python_lib-0.1.59 → sycommon_python_lib-0.2.0b1}/src/sycommon/rabbitmq/rabbitmq_client.py
RENAMED
|
@@ -117,37 +117,37 @@ 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:
|
|
128
|
-
# ===== 阶段 A:
|
|
128
|
+
# ===== 阶段 A: 检查状态与排队 =====
|
|
129
129
|
if await self.is_connected:
|
|
130
|
-
self._connect_condition.
|
|
130
|
+
if self._connect_condition.locked():
|
|
131
|
+
self._connect_condition.release()
|
|
131
132
|
return
|
|
132
133
|
|
|
134
|
+
# 如果已有协程正在连接,等待其完成
|
|
133
135
|
if self._connecting:
|
|
134
136
|
try:
|
|
135
137
|
logger.debug("连接正在进行中,等待现有连接完成...")
|
|
136
138
|
await asyncio.wait_for(self._connect_condition.wait(), timeout=60.0)
|
|
137
139
|
except asyncio.TimeoutError:
|
|
138
|
-
|
|
139
|
-
raise RuntimeError("等待连接超时")
|
|
140
|
+
logger.warning("等待前序连接超时,当前协程将尝试强制接管并重连...")
|
|
140
141
|
|
|
142
|
+
# 唤醒后再次检查状态,防止重复连接
|
|
141
143
|
if await self.is_connected:
|
|
142
|
-
self._connect_condition.
|
|
144
|
+
if self._connect_condition.locked():
|
|
145
|
+
self._connect_condition.release()
|
|
143
146
|
return
|
|
144
|
-
else:
|
|
145
|
-
self._connect_condition.release()
|
|
146
|
-
raise RuntimeError("等待重连后,连接状态依然无效")
|
|
147
147
|
|
|
148
|
-
# ===== 阶段 B:
|
|
148
|
+
# ===== 阶段 B: 标记开始连接并释放锁 =====
|
|
149
|
+
# 释放锁是为了让耗时的连接过程不阻塞其他协程
|
|
149
150
|
self._connecting = True
|
|
150
|
-
# 【关键】释放锁,允许其他协程进入等待逻辑
|
|
151
151
|
self._connect_condition.release()
|
|
152
152
|
|
|
153
153
|
except Exception as e:
|
|
@@ -155,13 +155,19 @@ class RabbitMQClient:
|
|
|
155
155
|
self._connect_condition.release()
|
|
156
156
|
raise
|
|
157
157
|
|
|
158
|
-
# === 阶段 C: 执行耗时的连接逻辑 (
|
|
158
|
+
# === 阶段 C: 执行耗时的连接逻辑 (无锁状态) ===
|
|
159
|
+
connection_failed = False
|
|
160
|
+
was_consuming = False
|
|
161
|
+
|
|
162
|
+
# 判断当前是否为消费者模式(通过是否有消息处理函数判断)
|
|
163
|
+
is_consumer = self._message_handler is not None
|
|
164
|
+
old_channel = self._channel
|
|
165
|
+
|
|
159
166
|
try:
|
|
160
|
-
# --- 步骤 1:
|
|
161
|
-
# 必须在清理前记录状态
|
|
167
|
+
# --- 步骤 1: 记录状态并清理旧资源 ---
|
|
162
168
|
was_consuming = self._consumer_tag is not None
|
|
163
169
|
|
|
164
|
-
#
|
|
170
|
+
# 清理旧连接的 close_callbacks,防止重连触发多次
|
|
165
171
|
if self._channel_conn:
|
|
166
172
|
try:
|
|
167
173
|
if self._channel_conn.close_callbacks:
|
|
@@ -169,60 +175,125 @@ class RabbitMQClient:
|
|
|
169
175
|
except Exception:
|
|
170
176
|
pass
|
|
171
177
|
|
|
172
|
-
#
|
|
178
|
+
# 显式关闭旧 Channel
|
|
179
|
+
# 注意:无论是生产者复用的主通道,还是消费者的独立通道,断开时都应显式关闭以释放服务端资源
|
|
180
|
+
if old_channel and not old_channel.is_closed:
|
|
181
|
+
try:
|
|
182
|
+
await old_channel.close()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# 【修复点】强制重置所有核心资源引用
|
|
187
|
+
# 因为我们即将获取一个新的 Channel,旧的 Exchange 和 Queue 对象(基于旧 Channel)将全部失效。
|
|
188
|
+
# 必须置为 None,强制后续逻辑基于新 Channel 重建这些对象。
|
|
173
189
|
self._channel = None
|
|
174
190
|
self._channel_conn = None
|
|
175
191
|
self._exchange = None
|
|
176
192
|
self._queue = None
|
|
177
193
|
self._consumer_tag = None
|
|
178
194
|
|
|
179
|
-
# --- 步骤 2:
|
|
180
|
-
|
|
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()
|
|
205
|
+
|
|
206
|
+
# --- 步骤 3: 设置连接关闭回调 ---
|
|
207
|
+
loop = asyncio.get_running_loop()
|
|
181
208
|
|
|
182
|
-
# 设置连接关闭回调
|
|
183
209
|
def on_conn_closed(conn, exc):
|
|
210
|
+
if self._closed:
|
|
211
|
+
return
|
|
184
212
|
logger.warning(f"检测到底层连接关闭: {exc}")
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
# 确保在循环中安全调用协程
|
|
214
|
+
asyncio.run_coroutine_threadsafe(self._safe_reconnect(), loop)
|
|
187
215
|
|
|
188
216
|
if self._channel_conn:
|
|
189
217
|
self._channel_conn.close_callbacks.add(on_conn_closed)
|
|
190
218
|
|
|
191
|
-
# --- 步骤
|
|
219
|
+
# --- 步骤 4: 重建基础资源 ---
|
|
220
|
+
# 这会在新的 self._channel 上声明 Exchange 和 Queue,并执行绑定
|
|
192
221
|
await self._rebuild_resources()
|
|
193
222
|
|
|
194
|
-
# --- 步骤 4: 恢复消费 ---
|
|
195
|
-
if was_consuming and self._message_handler:
|
|
196
|
-
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
|
|
197
|
-
try:
|
|
198
|
-
# 直接调用 start_consuming 来恢复,它内部包含了完整的队列检查和绑定逻辑
|
|
199
|
-
self._consumer_tag = await self.start_consuming()
|
|
200
|
-
logger.info(f"✅ 消费已自动恢复: {self._consumer_tag}")
|
|
201
|
-
except Exception as e:
|
|
202
|
-
logger.error(f"❌ 自动恢复消费失败: {e}")
|
|
203
|
-
self._consumer_tag = None
|
|
204
|
-
|
|
205
|
-
logger.info("客户端连接初始化完成")
|
|
206
|
-
|
|
207
223
|
except Exception as e:
|
|
224
|
+
connection_failed = True
|
|
208
225
|
logger.error(f"客户端连接失败: {str(e)}", exc_info=True)
|
|
209
|
-
|
|
226
|
+
|
|
227
|
+
# 发生异常时清理引用
|
|
210
228
|
if self._channel_conn and self._channel_conn.close_callbacks:
|
|
211
229
|
self._channel_conn.close_callbacks.clear()
|
|
230
|
+
|
|
212
231
|
self._channel = None
|
|
213
232
|
self._channel_conn = None
|
|
233
|
+
self._exchange = None
|
|
214
234
|
self._queue = None
|
|
215
235
|
self._consumer_tag = None
|
|
236
|
+
|
|
216
237
|
raise
|
|
217
238
|
|
|
218
239
|
finally:
|
|
219
|
-
# === 阶段 D:
|
|
220
|
-
await self._connect_condition.acquire()
|
|
240
|
+
# === 阶段 D: 恢复消费与收尾 (重新加锁) ===
|
|
221
241
|
try:
|
|
242
|
+
await self._connect_condition.acquire()
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
# 只有连接完全成功,且之前处于消费状态,才尝试自动恢复消费
|
|
248
|
+
if not connection_failed and was_consuming and self._message_handler:
|
|
249
|
+
logger.info("🔄 检测到重连前处于消费状态,尝试自动恢复消费...")
|
|
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
|
+
|
|
290
|
+
finally:
|
|
291
|
+
# 最终状态复位
|
|
222
292
|
self._connecting = False
|
|
223
293
|
self._connect_condition.notify_all()
|
|
224
|
-
|
|
225
|
-
self._connect_condition.
|
|
294
|
+
|
|
295
|
+
if self._connect_condition.locked():
|
|
296
|
+
self._connect_condition.release()
|
|
226
297
|
|
|
227
298
|
async def _safe_reconnect(self):
|
|
228
299
|
"""安全重连任务(仅用于被动监听连接关闭)"""
|
|
@@ -313,6 +384,11 @@ class RabbitMQClient:
|
|
|
313
384
|
self._consumer_tag = None
|
|
314
385
|
|
|
315
386
|
async def _handle_publish_failure(self):
|
|
387
|
+
# 如果当前正在重连,或者已经关闭,直接返回,避免冲突
|
|
388
|
+
if self._connecting or self._closed:
|
|
389
|
+
logger.warning("⚠️ 正在重连或已关闭,跳过故障转移触发")
|
|
390
|
+
return
|
|
391
|
+
|
|
316
392
|
try:
|
|
317
393
|
logger.info("检测到发布异常,强制连接池切换节点...")
|
|
318
394
|
await self.connection_pool.force_reconnect()
|
|
@@ -387,9 +463,12 @@ class RabbitMQClient:
|
|
|
387
463
|
raise RuntimeError(f"消息发布最终失败: {last_exception}")
|
|
388
464
|
|
|
389
465
|
async def close(self) -> None:
|
|
466
|
+
"""关闭客户端(支持独立通道的清理与死锁修复)"""
|
|
467
|
+
# 1. 先标记关闭
|
|
390
468
|
self._closed = True
|
|
391
469
|
logger.info("开始关闭RabbitMQ客户端...")
|
|
392
470
|
|
|
471
|
+
# 2. 取消可能存在的后台重连任务
|
|
393
472
|
if self._current_reconnect_task and not self._current_reconnect_task.done():
|
|
394
473
|
self._current_reconnect_task.cancel()
|
|
395
474
|
try:
|
|
@@ -397,21 +476,53 @@ class RabbitMQClient:
|
|
|
397
476
|
except asyncio.CancelledError:
|
|
398
477
|
pass
|
|
399
478
|
|
|
479
|
+
# 3. 停止消费
|
|
400
480
|
await self.stop_consuming()
|
|
401
481
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
482
|
+
# 4. 处理 _connect_condition 锁
|
|
483
|
+
try:
|
|
484
|
+
await asyncio.wait_for(self._connect_condition.acquire(), timeout=2.0)
|
|
485
|
+
except asyncio.TimeoutError:
|
|
486
|
+
logger.warning("获取连接锁超时,强制清理资源...")
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
# 清理回调
|
|
490
|
+
if self._channel_conn:
|
|
491
|
+
try:
|
|
492
|
+
if self._channel_conn.close_callbacks:
|
|
493
|
+
self._channel_conn.close_callbacks.clear()
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
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}")
|
|
406
511
|
|
|
512
|
+
# 置空资源引用
|
|
407
513
|
self._channel = None
|
|
408
514
|
self._channel_conn = None
|
|
409
515
|
self._exchange = None
|
|
410
516
|
self._queue = None
|
|
411
517
|
self._message_handler = None
|
|
518
|
+
self._conn_close_callback = None
|
|
412
519
|
|
|
413
|
-
|
|
520
|
+
finally:
|
|
521
|
+
# 强制重置状态并唤醒所有等待者
|
|
414
522
|
self._connecting = False
|
|
415
523
|
self._connect_condition.notify_all()
|
|
416
524
|
|
|
525
|
+
if self._connect_condition.locked():
|
|
526
|
+
self._connect_condition.release()
|
|
527
|
+
|
|
417
528
|
logger.info("客户端已关闭")
|