sycommon-python-lib 0.1.56b6__py3-none-any.whl → 0.1.56b8__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 (27) hide show
  1. sycommon/config/Config.py +17 -3
  2. sycommon/config/SentryConfig.py +13 -0
  3. sycommon/logging/kafka_log.py +185 -432
  4. sycommon/middleware/exception.py +10 -16
  5. sycommon/middleware/timeout.py +2 -1
  6. sycommon/middleware/traceid.py +67 -61
  7. sycommon/notice/uvicorn_monitor.py +32 -27
  8. sycommon/rabbitmq/rabbitmq_service.py +25 -854
  9. sycommon/rabbitmq/rabbitmq_service_client_manager.py +212 -0
  10. sycommon/rabbitmq/rabbitmq_service_connection_monitor.py +73 -0
  11. sycommon/rabbitmq/rabbitmq_service_consumer_manager.py +283 -0
  12. sycommon/rabbitmq/rabbitmq_service_core.py +117 -0
  13. sycommon/rabbitmq/rabbitmq_service_producer_manager.py +235 -0
  14. sycommon/sentry/__init__.py +0 -0
  15. sycommon/sentry/sy_sentry.py +34 -0
  16. sycommon/services.py +4 -0
  17. sycommon/synacos/nacos_client_base.py +119 -0
  18. sycommon/synacos/nacos_config_manager.py +106 -0
  19. sycommon/synacos/nacos_heartbeat_manager.py +142 -0
  20. sycommon/synacos/nacos_service.py +59 -780
  21. sycommon/synacos/nacos_service_discovery.py +138 -0
  22. sycommon/synacos/nacos_service_registration.py +252 -0
  23. {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b8.dist-info}/METADATA +2 -1
  24. {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b8.dist-info}/RECORD +27 -14
  25. {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b8.dist-info}/WHEEL +0 -0
  26. {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b8.dist-info}/entry_points.txt +0 -0
  27. {sycommon_python_lib-0.1.56b6.dist-info → sycommon_python_lib-0.1.56b8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,235 @@
1
+ from typing import List, Optional, Union, Dict, Any
2
+ import asyncio
3
+ import json
4
+ import time
5
+ from pydantic import BaseModel
6
+
7
+ from sycommon.logging.kafka_log import SYLogger
8
+ from sycommon.models.mqsend_config import RabbitMQSendConfig
9
+ from sycommon.config.Config import Config
10
+ from sycommon.models.sso_user import SsoUser
11
+ from sycommon.models.mqmsg_model import MQMsgModel
12
+ from sycommon.rabbitmq.rabbitmq_client import RabbitMQClient
13
+ from sycommon.rabbitmq.rabbitmq_service_client_manager import RabbitMQClientManager
14
+
15
+ logger = SYLogger
16
+
17
+
18
+ class RabbitMQProducerManager(RabbitMQClientManager):
19
+ """
20
+ RabbitMQ生产者管理类 - 负责发送器设置、消息发送
21
+ """
22
+ # 发送器相关状态
23
+ _sender_client_names: List[str] = []
24
+
25
+ @classmethod
26
+ async def setup_senders(cls, senders: List[RabbitMQSendConfig], has_listeners: bool = False, **kwargs) -> None:
27
+ """设置消息发送器"""
28
+ if cls._is_shutdown:
29
+ logger.warning("服务已关闭,无法设置发送器")
30
+ return
31
+
32
+ cls.set_mode_flags(has_listeners=has_listeners, has_senders=True)
33
+ logger.info(f"开始设置 {len(senders)} 个消息发送器,纯发送器模式: {not has_listeners}")
34
+
35
+ for idx, sender_config in enumerate(senders):
36
+ try:
37
+ if not sender_config.queue_name:
38
+ raise ValueError(f"发送器配置第{idx+1}项缺少queue_name")
39
+
40
+ prefetch_count = sender_config.prefetch_count
41
+ queue_name = sender_config.queue_name
42
+ app_name = cls._config.get(
43
+ "APP_NAME", "") if cls._config else ""
44
+
45
+ # 处理发送器队列名称
46
+ normalized_name = queue_name
47
+ if app_name and normalized_name.endswith(f".{app_name}"):
48
+ normalized_name = normalized_name[:-len(f".{app_name}")]
49
+ logger.info(f"发送器队列名称移除app-name后缀: {normalized_name}")
50
+
51
+ # 检查是否已初始化
52
+ if normalized_name in cls._sender_client_names:
53
+ logger.info(f"发送客户端 '{normalized_name}' 已存在,跳过")
54
+ continue
55
+
56
+ # 获取或创建客户端
57
+ if normalized_name in cls._clients:
58
+ client = cls._clients[normalized_name]
59
+ if not await client.is_connected:
60
+ client.create_if_not_exists = False
61
+ await client.connect()
62
+ else:
63
+ client = await cls.get_client(
64
+ client_name=normalized_name,
65
+ exchange_type=sender_config.exchange_type,
66
+ durable=sender_config.durable,
67
+ auto_delete=sender_config.auto_delete,
68
+ auto_parse_json=sender_config.auto_parse_json,
69
+ queue_name=queue_name,
70
+ create_if_not_exists=False,
71
+ prefetch_count=prefetch_count, ** kwargs
72
+ )
73
+
74
+ # 记录客户端
75
+ if normalized_name not in cls._clients:
76
+ cls._clients[normalized_name] = client
77
+ logger.info(f"发送客户端 '{normalized_name}' 已添加")
78
+
79
+ if normalized_name not in cls._sender_client_names:
80
+ cls._sender_client_names.append(normalized_name)
81
+ logger.info(f"发送客户端 '{normalized_name}' 初始化成功(纯发送器模式)")
82
+
83
+ except Exception as e:
84
+ logger.error(
85
+ f"初始化发送客户端第{idx+1}项失败: {str(e)}", exc_info=True)
86
+
87
+ logger.info(
88
+ f"消息发送器设置完成,共 {len(cls._sender_client_names)} 个发送器,纯发送器模式: {not has_listeners}")
89
+
90
+ @classmethod
91
+ async def get_sender(cls, queue_name: str) -> Optional[RabbitMQClient]:
92
+ """获取发送客户端"""
93
+ if cls._is_shutdown:
94
+ logger.warning("服务已关闭,无法获取发送器")
95
+ return None
96
+
97
+ if not queue_name:
98
+ logger.warning("发送器名称不能为空")
99
+ return None
100
+
101
+ # 检查是否在已注册的发送器中
102
+ if queue_name in cls._sender_client_names and queue_name in cls._clients:
103
+ client = cls._clients[queue_name]
104
+ if await client.is_connected:
105
+ return client
106
+ else:
107
+ logger.info(f"发送器 '{queue_name}' 连接已断开,尝试重连")
108
+ try:
109
+ client.create_if_not_exists = False
110
+ await client.connect()
111
+ if await client.is_connected:
112
+ return client
113
+ except Exception as e:
114
+ logger.error(f"发送器 '{queue_name}' 重连失败: {str(e)}")
115
+ return None
116
+
117
+ # 检查是否带有app-name后缀
118
+ app_name = cls._config.get("APP_NAME", "") if cls._config else ""
119
+ if app_name:
120
+ suffixed_name = f"{queue_name}.{app_name}"
121
+ if suffixed_name in cls._sender_client_names and suffixed_name in cls._clients:
122
+ client = cls._clients[suffixed_name]
123
+ if await client.is_connected:
124
+ return client
125
+ else:
126
+ logger.info(f"发送器 '{suffixed_name}' 连接已断开,尝试重连")
127
+ try:
128
+ client.create_if_not_exists = False
129
+ await client.connect()
130
+ if await client.is_connected:
131
+ return client
132
+ except Exception as e:
133
+ logger.error(f"发送器 '{suffixed_name}' 重连失败: {str(e)}")
134
+
135
+ logger.info(f"未找到可用的发送器 '{queue_name}'")
136
+ return None
137
+
138
+ @classmethod
139
+ async def send_message(
140
+ cls,
141
+ data: Union[BaseModel, str, Dict[str, Any], None],
142
+ queue_name: str, **kwargs
143
+ ) -> None:
144
+ """发送消息到指定队列"""
145
+ if cls._is_shutdown:
146
+ raise RuntimeError("RabbitMQService已关闭,无法发送消息")
147
+
148
+ # 获取发送客户端
149
+ sender = await cls.get_sender(queue_name)
150
+ if not sender:
151
+ error_msg = f"未找到可用的RabbitMQ发送器 (queue_name: {queue_name})"
152
+ logger.error(error_msg)
153
+ raise ValueError(error_msg)
154
+
155
+ # 确保连接有效
156
+ if not await sender.is_connected:
157
+ logger.info(f"发送器 '{queue_name}' 连接已关闭,尝试重新连接")
158
+ max_retry = 3
159
+ retry_count = 0
160
+ last_exception = None
161
+
162
+ while retry_count < max_retry and not cls._is_shutdown:
163
+ try:
164
+ sender.create_if_not_exists = False
165
+ await sender.connect()
166
+ if await sender.is_connected:
167
+ logger.info(
168
+ f"发送器 '{queue_name}' 第 {retry_count + 1} 次重连成功")
169
+ break
170
+ except Exception as e:
171
+ last_exception = e
172
+ retry_count += 1
173
+ logger.warning(
174
+ f"发送器 '{queue_name}' 第 {retry_count} 次重连失败: {str(e)}")
175
+ await asyncio.sleep(cls.RECONNECT_INTERVAL)
176
+
177
+ if retry_count >= max_retry and not await sender.is_connected:
178
+ error_msg = f"发送器 '{queue_name}' 经过 {max_retry} 次重连仍失败"
179
+ logger.error(f"{error_msg}: {str(last_exception)}")
180
+ raise Exception(error_msg) from last_exception
181
+
182
+ try:
183
+ # 处理消息数据
184
+ msg_content = ""
185
+ if isinstance(data, str):
186
+ msg_content = data
187
+ elif isinstance(data, BaseModel):
188
+ msg_content = data.model_dump_json()
189
+ elif isinstance(data, dict):
190
+ msg_content = json.dumps(data, ensure_ascii=False)
191
+
192
+ # 创建标准消息模型
193
+ mq_message = MQMsgModel(
194
+ topicCode=queue_name.split('.')[0] if queue_name else "",
195
+ msg=msg_content,
196
+ correlationDataId=kwargs.get(
197
+ 'correlationDataId', logger.get_trace_id()),
198
+ groupId=kwargs.get('groupId', ''),
199
+ dataKey=kwargs.get('dataKey', ""),
200
+ manualFlag=kwargs.get('manualFlag', False),
201
+ traceId=logger.get_trace_id()
202
+ )
203
+
204
+ # 构建消息头
205
+ namespaceId = Config().config.get('Nacos', {}).get('namespaceId', '')
206
+ tenant_id = "T000002" if namespaceId == "prod" or namespaceId == "wsuat1" else "T000003"
207
+ mq_header = {
208
+ "context": SsoUser(
209
+ tenant_id=tenant_id,
210
+ customer_id="SYSTEM",
211
+ user_id="SYSTEM",
212
+ user_name="SYSTEM",
213
+ request_path="/",
214
+ req_type="SYSTEM",
215
+ trace_id=logger.get_trace_id(),
216
+ ).model_dump_json(),
217
+ "tenant_id": logger.get_trace_id(),
218
+ "createTime": str(int(time.time() * 1000)),
219
+ }
220
+
221
+ # 发送消息
222
+ await sender.publish(
223
+ message_body=mq_message.model_dump_json(),
224
+ headers=mq_header,
225
+ content_type="application/json"
226
+ )
227
+ logger.info(f"消息发送成功 (队列: {queue_name})")
228
+ except Exception as e:
229
+ logger.error(f"消息发送失败: {str(e)}", exc_info=True)
230
+ raise
231
+
232
+ @classmethod
233
+ def clear_senders(cls) -> None:
234
+ """清理发送器状态"""
235
+ cls._sender_client_names.clear()
File without changes
@@ -0,0 +1,34 @@
1
+ import sentry_sdk
2
+ from datetime import datetime
3
+ from sycommon.config.Config import Config
4
+ from sycommon.logging.kafka_log import SYLogger
5
+ from sentry_sdk.integrations.fastapi import FastApiIntegration
6
+ # from sentry_sdk.integrations.logging import LoggingIntegration
7
+
8
+
9
+ def sy_sentry_init():
10
+ config = Config().config
11
+ server_name = config.get('Name', '')
12
+ environment = config.get('Nacos', {}).get('namespaceId', '')
13
+ sentry_configs = config.get('SentryConfig', [])
14
+ target_config = next(
15
+ (item for item in sentry_configs if item.get('name') == server_name), None)
16
+ target_dsn = target_config.get('dsn')
17
+ target_enable = target_config.get('enable')
18
+ current_version = datetime.now().strftime("%Y-%m-%d %H:%M:%S-version")
19
+ if target_config and target_dsn and target_enable:
20
+ try:
21
+ sentry_sdk.init(
22
+ dsn=target_dsn,
23
+ traces_sample_rate=1.0,
24
+ server_name=server_name,
25
+ environment=environment,
26
+ release=current_version,
27
+ integrations=[
28
+ FastApiIntegration(),
29
+ # LoggingIntegration(level=logging.INFO,
30
+ # event_level=logging.ERROR)
31
+ ],
32
+ )
33
+ except Exception as e:
34
+ SYLogger.error(f"Sentry初始化失败: {str(e)}")
sycommon/services.py CHANGED
@@ -12,6 +12,7 @@ from sycommon.models.mqlistener_config import RabbitMQListenerConfig
12
12
  from sycommon.models.mqsend_config import RabbitMQSendConfig
13
13
  from sycommon.rabbitmq.rabbitmq_service import RabbitMQService
14
14
  from sycommon.tools.docs import custom_redoc_html, custom_swagger_ui_html
15
+ from sycommon.sentry.sy_sentry import sy_sentry_init
15
16
 
16
17
 
17
18
  class Services(metaclass=SingletonMeta):
@@ -87,6 +88,9 @@ class Services(metaclass=SingletonMeta):
87
88
  if logging_service:
88
89
  logging_service(config)
89
90
 
91
+ # 设置sentry
92
+ sy_sentry_init()
93
+
90
94
  # ========== 处理数据库服务 ==========
91
95
  # 清空之前的待执行列表(防止热重载时重复)
92
96
  cls._pending_async_db_setup = []
@@ -0,0 +1,119 @@
1
+ import threading
2
+ import time
3
+ from typing import Optional
4
+ import nacos
5
+ from sycommon.logging.kafka_log import SYLogger
6
+
7
+
8
+ class NacosClientBase:
9
+ """Nacos客户端基础类 - 负责客户端初始化和连接管理"""
10
+
11
+ def __init__(self, nacos_config: dict, enable_register_nacos: bool):
12
+ self.nacos_config = nacos_config
13
+ self.enable_register_nacos = enable_register_nacos
14
+
15
+ # 客户端配置
16
+ self.max_retries = self.nacos_config.get('maxRetries', 5)
17
+ self.retry_delay = self.nacos_config.get('retryDelay', 5)
18
+ self.max_retry_delay = self.nacos_config.get('maxRetryDelay', 30)
19
+
20
+ # 状态管理
21
+ self._client_initialized = False
22
+ self._state_lock = threading.RLock()
23
+ self._shutdown_event = threading.Event()
24
+ self.nacos_client: Optional[nacos.NacosClient] = None
25
+
26
+ def _initialize_client(self) -> bool:
27
+ """初始化Nacos客户端(仅首次调用时执行)"""
28
+ if self._client_initialized:
29
+ return True
30
+
31
+ for attempt in range(self.max_retries):
32
+ try:
33
+ register_ip = self.nacos_config['registerIp']
34
+ namespace_id = self.nacos_config['namespaceId']
35
+ self.nacos_client = nacos.NacosClient(
36
+ server_addresses=register_ip,
37
+ namespace=namespace_id
38
+ )
39
+ SYLogger.info("nacos:客户端初始化成功")
40
+ self._client_initialized = True
41
+ return True
42
+ except Exception as e:
43
+ delay = min(self.retry_delay, self.max_retry_delay)
44
+ SYLogger.error(
45
+ f"nacos:客户端初始化失败 (尝试 {attempt+1}/{self.max_retries}): {e}")
46
+ time.sleep(delay)
47
+
48
+ SYLogger.warning("nacos:无法连接到 Nacos 服务器,已达到最大重试次数")
49
+ return False
50
+
51
+ def ensure_client_connected(self, retry_once: bool = False) -> bool:
52
+ """确保Nacos客户端已连接,返回连接状态"""
53
+ with self._state_lock:
54
+ if self._client_initialized:
55
+ return True
56
+
57
+ SYLogger.warning("nacos:客户端未初始化,尝试连接...")
58
+
59
+ max_attempts = 2 if retry_once else self.max_retries
60
+ attempt = 0
61
+
62
+ while attempt < max_attempts:
63
+ try:
64
+ register_ip = self.nacos_config['registerIp']
65
+ namespace_id = self.nacos_config['namespaceId']
66
+
67
+ self.nacos_client = nacos.NacosClient(
68
+ server_addresses=register_ip,
69
+ namespace=namespace_id
70
+ )
71
+
72
+ if self._verify_client_connection():
73
+ with self._state_lock:
74
+ self._client_initialized = True
75
+ SYLogger.info("nacos:客户端初始化成功")
76
+ return True
77
+ else:
78
+ raise ConnectionError("nacos:客户端初始化后无法验证连接")
79
+
80
+ except Exception as e:
81
+ attempt += 1
82
+ delay = min(self.retry_delay, self.max_retry_delay)
83
+ SYLogger.error(
84
+ f"nacos:客户端初始化失败 (尝试 {attempt}/{max_attempts}): {e}")
85
+ time.sleep(delay)
86
+
87
+ SYLogger.error("nacos:无法连接到 Nacos 服务器,已达到最大重试次数")
88
+ return False
89
+
90
+ def _verify_client_connection(self) -> bool:
91
+ """验证客户端是否真正连接成功"""
92
+ if not self.enable_register_nacos:
93
+ return True
94
+
95
+ try:
96
+ namespace_id = self.nacos_config['namespaceId']
97
+ self.nacos_client.list_naming_instance(
98
+ service_name="", # 空服务名仅用于验证连接
99
+ namespace_id=namespace_id,
100
+ group_name="DEFAULT_GROUP",
101
+ healthy_only=True
102
+ )
103
+ return True
104
+ except Exception as e:
105
+ SYLogger.warning(f"nacos:客户端连接验证失败: {e}")
106
+ return False
107
+
108
+ def reconnect_nacos_client(self) -> bool:
109
+ """重新连接Nacos客户端"""
110
+ SYLogger.warning("nacos:尝试重新连接Nacos客户端")
111
+ with self._state_lock:
112
+ self._client_initialized = False
113
+ return self.ensure_client_connected()
114
+
115
+ @property
116
+ def is_connected(self) -> bool:
117
+ """检查客户端是否已连接"""
118
+ with self._state_lock:
119
+ return self._client_initialized
@@ -0,0 +1,106 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ from typing import Callable, Optional, Dict, List
5
+ import yaml
6
+ from sycommon.logging.kafka_log import SYLogger
7
+
8
+
9
+ class NacosConfigManager:
10
+ """Nacos配置管理类 - 负责配置读取、监听和更新"""
11
+
12
+ def __init__(self, client_base):
13
+ self.client_base = client_base
14
+
15
+ # 配置
16
+ self.config_watch_interval = self.client_base.nacos_config.get(
17
+ 'configWatchInterval', 30)
18
+
19
+ # 状态
20
+ self.share_configs: Dict = {}
21
+ self._config_listeners: Dict[str, Callable[[str], None]] = {}
22
+ self._config_cache: Dict[str, str] = {}
23
+ self._watch_thread: Optional[threading.Thread] = None
24
+
25
+ def read_configs(self, shared_configs: List[Dict]) -> dict:
26
+ """读取共享配置"""
27
+ configs = {}
28
+
29
+ for config in shared_configs:
30
+ data_id = config['dataId']
31
+ group = config['group']
32
+
33
+ for attempt in range(self.client_base.max_retries):
34
+ try:
35
+ if not self.client_base.ensure_client_connected():
36
+ self.client_base.reconnect_nacos_client()
37
+
38
+ content = self.client_base.nacos_client.get_config(
39
+ data_id, group)
40
+
41
+ try:
42
+ configs[data_id] = json.loads(content)
43
+ except json.JSONDecodeError:
44
+ try:
45
+ configs[data_id] = yaml.safe_load(content)
46
+ except yaml.YAMLError:
47
+ SYLogger.error(f"nacos:无法解析 {data_id} 的内容")
48
+ break
49
+ except Exception as e:
50
+ if attempt < self.client_base.max_retries - 1:
51
+ SYLogger.warning(
52
+ f"nacos:读取配置 {data_id} 失败 (尝试 {attempt+1}/{self.client_base.max_retries}): {e}")
53
+ time.sleep(self.client_base.retry_delay)
54
+ else:
55
+ SYLogger.error(
56
+ f"nacos:读取配置 {data_id} 失败,已达到最大重试次数: {e}")
57
+
58
+ self.share_configs = configs
59
+ return configs
60
+
61
+ def add_config_listener(self, data_id: str, callback: Callable[[str], None]):
62
+ """添加配置变更监听器"""
63
+ self._config_listeners[data_id] = callback
64
+ if config := self.get_config(data_id):
65
+ callback(config)
66
+
67
+ def get_config(self, data_id: str, group: str = "DEFAULT_GROUP") -> Optional[str]:
68
+ """获取配置内容"""
69
+ if not self.client_base.ensure_client_connected():
70
+ return None
71
+
72
+ try:
73
+ return self.client_base.nacos_client.get_config(data_id, group=group)
74
+ except Exception as e:
75
+ SYLogger.error(f"nacos:获取配置 {data_id} 失败: {str(e)}")
76
+ return None
77
+
78
+ def start_watch_configs(self):
79
+ """启动配置监视线程"""
80
+ self._watch_thread = threading.Thread(
81
+ target=self._watch_configs, daemon=True)
82
+ self._watch_thread.start()
83
+
84
+ def _watch_configs(self):
85
+ """配置监听线程"""
86
+ check_interval = self.config_watch_interval
87
+
88
+ while not self.client_base._shutdown_event.is_set():
89
+ try:
90
+ for data_id, callback in list(self._config_listeners.items()):
91
+ new_config = self.get_config(data_id)
92
+ if new_config and new_config != self._config_cache.get(data_id):
93
+ callback(new_config)
94
+ self._config_cache[data_id] = new_config
95
+ try:
96
+ self.share_configs[data_id] = json.loads(
97
+ new_config)
98
+ except json.JSONDecodeError:
99
+ try:
100
+ self.share_configs[data_id] = yaml.safe_load(
101
+ new_config)
102
+ except yaml.YAMLError:
103
+ SYLogger.error(f"nacos:无法解析 {data_id} 的内容")
104
+ except Exception as e:
105
+ SYLogger.error(f"nacos:配置监视线程异常: {str(e)}")
106
+ self.client_base._shutdown_event.wait(check_interval)
@@ -0,0 +1,142 @@
1
+ import threading
2
+ import time
3
+ from sycommon.logging.kafka_log import SYLogger
4
+
5
+
6
+ class NacosHeartbeatManager:
7
+ """Nacos心跳管理类 - 负责心跳发送和监控"""
8
+
9
+ def __init__(self, client_base, registration, heartbeat_interval: int = 15):
10
+ self.client_base = client_base
11
+ self.registration = registration
12
+
13
+ # 心跳配置
14
+ self.heartbeat_interval = heartbeat_interval
15
+ self.heartbeat_timeout = 15
16
+ self.max_heartbeat_timeout = self.client_base.nacos_config.get(
17
+ 'maxHeartbeatTimeout', 30)
18
+
19
+ # 状态管理
20
+ self._heartbeat_lock = threading.Lock()
21
+ self._heartbeat_thread = None
22
+ self._last_heartbeat_time = 0
23
+ self._heartbeat_fail_count = 0
24
+
25
+ def start_heartbeat(self):
26
+ """启动心跳线程(确保单例)"""
27
+ with self._heartbeat_lock:
28
+ if self._heartbeat_thread is not None and self._heartbeat_thread.is_alive():
29
+ return
30
+
31
+ self._heartbeat_thread = None
32
+
33
+ self._heartbeat_thread = threading.Thread(
34
+ target=self._send_heartbeat_loop,
35
+ name="NacosHeartbeatThread",
36
+ daemon=True
37
+ )
38
+ self._heartbeat_thread.start()
39
+ SYLogger.info(
40
+ f"nacos:心跳线程启动,线程ID: {self._heartbeat_thread.ident},"
41
+ f"心跳间隔: {self.heartbeat_interval}秒,"
42
+ f"心跳超时: {self.heartbeat_timeout}秒"
43
+ )
44
+
45
+ def _send_heartbeat_loop(self):
46
+ """心跳发送循环"""
47
+ current_thread = threading.current_thread()
48
+ thread_ident = current_thread.ident
49
+ SYLogger.info(
50
+ f"nacos:心跳循环启动 - 线程ID: {thread_ident}, "
51
+ f"配置间隔: {self.heartbeat_interval}秒, "
52
+ f"超时时间: {self.heartbeat_timeout}秒"
53
+ )
54
+
55
+ consecutive_fail = 0
56
+
57
+ while not self.client_base._shutdown_event.is_set():
58
+ current_time = time.time()
59
+
60
+ try:
61
+ registered_status = self.registration.registered
62
+
63
+ if not registered_status:
64
+ SYLogger.warning(
65
+ f"nacos:服务未注册,跳过心跳 - 线程ID: {thread_ident}")
66
+ consecutive_fail = 0
67
+ else:
68
+ success = self.send_heartbeat()
69
+ if success:
70
+ consecutive_fail = 0
71
+ SYLogger.info(
72
+ f"nacos:心跳发送成功 - 时间: {current_time:.3f}, "
73
+ f"间隔: {self.heartbeat_interval}秒"
74
+ )
75
+ else:
76
+ consecutive_fail += 1
77
+ SYLogger.warning(
78
+ f"nacos:心跳发送失败 - 连续失败: {consecutive_fail}次"
79
+ )
80
+ if consecutive_fail >= 5:
81
+ SYLogger.error("nacos:心跳连续失败5次,尝试重连")
82
+ self.client_base.reconnect_nacos_client()
83
+ consecutive_fail = 0
84
+
85
+ except Exception as e:
86
+ consecutive_fail += 1
87
+ SYLogger.error(
88
+ f"nacos:心跳异常: {str(e)}, 连续失败: {consecutive_fail}次")
89
+
90
+ self.client_base._shutdown_event.wait(self.heartbeat_interval)
91
+
92
+ SYLogger.info(f"nacos:心跳循环已停止 - 线程ID: {thread_ident}")
93
+
94
+ def send_heartbeat(self) -> bool:
95
+ """发送心跳并添加超时控制"""
96
+ if not self.client_base.ensure_client_connected():
97
+ SYLogger.warning("nacos:客户端未连接,心跳发送失败")
98
+ return False
99
+
100
+ result_list = []
101
+
102
+ def heartbeat_task():
103
+ try:
104
+ result = self._send_heartbeat_internal()
105
+ result_list.append(result)
106
+ except Exception as e:
107
+ SYLogger.error(f"nacos:心跳任务执行异常: {e}")
108
+ result_list.append(False)
109
+
110
+ task_thread = threading.Thread(
111
+ target=heartbeat_task,
112
+ daemon=True,
113
+ name="NacosHeartbeatTaskThread"
114
+ )
115
+ task_thread.start()
116
+ task_thread.join(timeout=self.heartbeat_timeout)
117
+
118
+ if not result_list:
119
+ SYLogger.error(f"nacos:心跳发送超时({self.heartbeat_timeout}秒)")
120
+ self.client_base._client_initialized = False
121
+ return False
122
+
123
+ return result_list[0]
124
+
125
+ def _send_heartbeat_internal(self) -> bool:
126
+ """实际的心跳发送逻辑"""
127
+ result = self.client_base.nacos_client.send_heartbeat(
128
+ service_name=self.registration.service_name,
129
+ ip=self.registration.real_ip,
130
+ port=int(self.registration.port),
131
+ cluster_name="DEFAULT",
132
+ weight=1.0,
133
+ metadata={
134
+ "version": self.registration.version} if self.registration.version else None
135
+ )
136
+
137
+ if result and isinstance(result, dict) and result.get('lightBeatEnabled', False):
138
+ SYLogger.info(f"nacos:心跳发送成功,Nacos返回: {result}")
139
+ return True
140
+ else:
141
+ SYLogger.warning(f"nacos:心跳发送失败,Nacos返回: {result}")
142
+ return False