sycommon-python-lib 0.1.45__tar.gz → 0.1.46__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.45 → sycommon_python_lib-0.1.46}/PKG-INFO +1 -1
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/pyproject.toml +1 -1
- sycommon_python_lib-0.1.46/src/sycommon/rabbitmq/rabbitmq_pool.py +586 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/rabbitmq/rabbitmq_service.py +10 -6
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
- sycommon_python_lib-0.1.45/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -404
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/README.md +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/setup.cfg +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/command/cli.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/Config.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/DatabaseConfig.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/EmbeddingConfig.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/LLMConfig.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/MQConfig.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/RerankerConfig.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/database/base_db_service.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/database/database_service.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/health/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/health/health_check.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/health/metrics.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/health/ping.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/logging/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/logging/kafka_log.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/logging/logger_wrapper.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/logging/sql_logger.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/context.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/cors.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/docs.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/exception.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/middleware.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/monitor_memory.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/mq.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/timeout.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/traceid.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/base_http.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/log.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/mqlistener_config.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/mqmsg_model.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/mqsend_config.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/sso_user.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/rabbitmq/rabbitmq_client.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/services.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/sse/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/sse/event.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/sse/sse.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/example.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/example2.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/feign.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/feign_client.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/nacos_service.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/param.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/tools/__init__.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/tools/docs.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/tools/snowflake.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/tools/timing.py +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon_python_lib.egg-info/SOURCES.txt +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
- {sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional, List, Set, Iterator, Tuple, Dict, Callable
|
|
3
|
+
from aio_pika import connect_robust, Channel, Message
|
|
4
|
+
from aio_pika.abc import (
|
|
5
|
+
AbstractRobustConnection, AbstractQueue, AbstractExchange, AbstractMessage
|
|
6
|
+
)
|
|
7
|
+
from aio_pika.exceptions import ChannelClosed
|
|
8
|
+
import aiormq.exceptions
|
|
9
|
+
|
|
10
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
11
|
+
|
|
12
|
+
logger = SYLogger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RabbitMQConnectionPool:
|
|
16
|
+
"""单连接RabbitMQ通道池(核心特性:严格单连接+重连释放旧资源+新连接保留自动恢复+全场景加锁)"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
hosts: List[str],
|
|
21
|
+
port: int,
|
|
22
|
+
username: str,
|
|
23
|
+
password: str,
|
|
24
|
+
virtualhost: str = "/",
|
|
25
|
+
channel_pool_size: int = 1,
|
|
26
|
+
heartbeat: int = 30,
|
|
27
|
+
app_name: str = "",
|
|
28
|
+
connection_timeout: int = 30,
|
|
29
|
+
reconnect_interval: int = 30,
|
|
30
|
+
prefetch_count: int = 2,
|
|
31
|
+
):
|
|
32
|
+
# 基础配置校验与初始化
|
|
33
|
+
self.hosts = [host.strip() for host in hosts if host.strip()]
|
|
34
|
+
if not self.hosts:
|
|
35
|
+
raise ValueError("至少需要提供一个RabbitMQ主机地址")
|
|
36
|
+
|
|
37
|
+
self.port = port
|
|
38
|
+
self.username = username
|
|
39
|
+
self.password = password
|
|
40
|
+
self.virtualhost = virtualhost
|
|
41
|
+
self.app_name = app_name or "rabbitmq-client"
|
|
42
|
+
self.heartbeat = heartbeat
|
|
43
|
+
self.connection_timeout = connection_timeout
|
|
44
|
+
self.reconnect_interval = reconnect_interval
|
|
45
|
+
self.prefetch_count = prefetch_count
|
|
46
|
+
self.channel_pool_size = max(1, channel_pool_size) # 确保池大小不小于1
|
|
47
|
+
|
|
48
|
+
# 节点轮询(仅重连时切换)
|
|
49
|
+
self._host_iterator: Iterator[str] = self._create_host_iterator()
|
|
50
|
+
self._current_host: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
# 核心资源(严格单连接+通道池,仅绑定当前活跃连接)
|
|
53
|
+
self._connection: Optional[AbstractRobustConnection] = None # 唯一活跃连接
|
|
54
|
+
self._free_channels: List[Channel] = [] # 当前连接的空闲通道(带自动恢复)
|
|
55
|
+
self._used_channels: Set[Channel] = set() # 当前连接的使用中通道(带自动恢复)
|
|
56
|
+
self._consumer_channels: Dict[str, Tuple[Channel,
|
|
57
|
+
AbstractRobustConnection, Callable, bool, dict]] = {} # 消费者通道跟踪
|
|
58
|
+
|
|
59
|
+
# 状态控制(确保并发安全和单连接)
|
|
60
|
+
self._lock = asyncio.Lock() # 全局唯一锁,保护所有共享状态
|
|
61
|
+
self._initialized = False
|
|
62
|
+
self._is_shutdown = False
|
|
63
|
+
self._reconnecting = False # 避免并发重连
|
|
64
|
+
self._connection_version = 0 # 连接版本号(区分新旧连接/通道)
|
|
65
|
+
|
|
66
|
+
def _create_host_iterator(self) -> Iterator[str]:
|
|
67
|
+
"""创建无限循环的节点轮询迭代器"""
|
|
68
|
+
while True:
|
|
69
|
+
for host in self.hosts:
|
|
70
|
+
yield host
|
|
71
|
+
|
|
72
|
+
async def _is_connection_valid(self) -> bool:
|
|
73
|
+
"""原子化检查连接有效性(加锁保证无竞态)"""
|
|
74
|
+
async with self._lock:
|
|
75
|
+
return (
|
|
76
|
+
self._connection is not None
|
|
77
|
+
and not self._connection.is_closed
|
|
78
|
+
and not self._reconnecting
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
async def is_alive(self) -> bool:
|
|
83
|
+
"""对外暴露的连接存活状态(异步+原子化)"""
|
|
84
|
+
if self._is_shutdown:
|
|
85
|
+
return False
|
|
86
|
+
return await self._is_connection_valid()
|
|
87
|
+
|
|
88
|
+
async def _safe_close_old_resources(self):
|
|
89
|
+
"""强制关闭所有旧资源(加锁保证原子性,重连前必调用)"""
|
|
90
|
+
async with self._lock:
|
|
91
|
+
logger.info(f"开始释放旧资源(连接版本: {self._connection_version})...")
|
|
92
|
+
|
|
93
|
+
# 1. 关闭所有消费者通道(独立管理,终止旧自动恢复)
|
|
94
|
+
for queue_name, (channel, _, _, _, _) in self._consumer_channels.items():
|
|
95
|
+
try:
|
|
96
|
+
if not channel.is_closed:
|
|
97
|
+
await channel.close()
|
|
98
|
+
logger.info(f"已关闭队列 {queue_name} 的旧消费者通道(自动恢复终止)")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.warning(f"关闭消费者通道 {queue_name} 失败: {str(e)}")
|
|
101
|
+
self._consumer_channels.clear()
|
|
102
|
+
|
|
103
|
+
# 2. 关闭所有普通通道(空闲+使用中,终止旧自动恢复)
|
|
104
|
+
all_channels = self._free_channels + list(self._used_channels)
|
|
105
|
+
for channel in all_channels:
|
|
106
|
+
try:
|
|
107
|
+
if not channel.is_closed:
|
|
108
|
+
await channel.close()
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning(f"关闭旧通道失败: {str(e)}")
|
|
111
|
+
self._free_channels.clear()
|
|
112
|
+
self._used_channels.clear()
|
|
113
|
+
|
|
114
|
+
# 3. 强制关闭旧连接(彻底终止旧连接的所有自动恢复)
|
|
115
|
+
if self._connection:
|
|
116
|
+
try:
|
|
117
|
+
if not self._connection.is_closed:
|
|
118
|
+
await self._connection.close()
|
|
119
|
+
logger.info(
|
|
120
|
+
f"已关闭旧连接: {self._current_host}:{self.port}(版本: {self._connection_version})")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"关闭旧连接失败: {str(e)}")
|
|
123
|
+
self._connection = None # 置空,确保单连接
|
|
124
|
+
|
|
125
|
+
logger.info("旧资源释放完成(所有旧自动恢复逻辑已终止)")
|
|
126
|
+
|
|
127
|
+
async def _create_single_connection(self) -> AbstractRobustConnection:
|
|
128
|
+
"""创建唯一活跃连接(重连前已释放旧资源,确保单连接)"""
|
|
129
|
+
max_attempts = len(self.hosts) * 2 # 每个节点尝试2次
|
|
130
|
+
attempts = 0
|
|
131
|
+
last_error: Optional[Exception] = None
|
|
132
|
+
|
|
133
|
+
while attempts < max_attempts and not self._is_shutdown:
|
|
134
|
+
self._current_host = next(self._host_iterator)
|
|
135
|
+
conn_url = f"amqp://{self.username}:{self.password}@{self._current_host}:{self.port}/{self.virtualhost}"
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
target_version = self._connection_version + 1
|
|
139
|
+
logger.info(
|
|
140
|
+
f"尝试创建连接: {self._current_host}:{self.port} "
|
|
141
|
+
f"(目标版本: {target_version},{attempts+1}/{max_attempts})"
|
|
142
|
+
)
|
|
143
|
+
# 创建连接(保留aio-pika原生自动恢复)
|
|
144
|
+
conn = await connect_robust(
|
|
145
|
+
conn_url,
|
|
146
|
+
properties={
|
|
147
|
+
"connection_name": f"{self.app_name}_conn_v{target_version}",
|
|
148
|
+
"product": self.app_name
|
|
149
|
+
},
|
|
150
|
+
heartbeat=self.heartbeat,
|
|
151
|
+
timeout=self.connection_timeout,
|
|
152
|
+
reconnect_interval=5, # 单节点内部短间隔重连(原生自动恢复)
|
|
153
|
+
max_reconnect_attempts=3, # 单节点最大重试3次
|
|
154
|
+
)
|
|
155
|
+
logger.info(
|
|
156
|
+
f"连接创建成功: {self._current_host}:{self.port}(版本: {target_version})")
|
|
157
|
+
return conn
|
|
158
|
+
except Exception as e:
|
|
159
|
+
attempts += 1
|
|
160
|
+
last_error = e
|
|
161
|
+
logger.error(
|
|
162
|
+
f"连接节点 {self._current_host}:{self.port} 失败({attempts}/{max_attempts}): {str(e)}",
|
|
163
|
+
exc_info=True
|
|
164
|
+
)
|
|
165
|
+
await asyncio.sleep(min(5 * attempts, self.reconnect_interval))
|
|
166
|
+
|
|
167
|
+
raise ConnectionError(
|
|
168
|
+
f"所有节点创建连接失败(节点列表: {self.hosts})"
|
|
169
|
+
) from last_error
|
|
170
|
+
|
|
171
|
+
async def _init_channel_pool(self):
|
|
172
|
+
"""初始化通道池(加锁保证原子性,绑定当前连接)"""
|
|
173
|
+
async with self._lock:
|
|
174
|
+
if self._is_shutdown:
|
|
175
|
+
raise RuntimeError("通道池已关闭,无法初始化")
|
|
176
|
+
|
|
177
|
+
# 校验当前连接有效性
|
|
178
|
+
if not self._connection or self._connection.is_closed:
|
|
179
|
+
raise RuntimeError("无有效连接,无法初始化通道池")
|
|
180
|
+
|
|
181
|
+
self._free_channels.clear()
|
|
182
|
+
self._used_channels.clear()
|
|
183
|
+
|
|
184
|
+
# 创建指定数量的通道(保留原生自动恢复)
|
|
185
|
+
for i in range(self.channel_pool_size):
|
|
186
|
+
try:
|
|
187
|
+
channel = await self._connection.channel() # 新通道自带自动恢复
|
|
188
|
+
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
189
|
+
self._free_channels.append(channel)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(
|
|
192
|
+
f"创建通道失败(第{i+1}个,连接版本: {self._connection_version}): {str(e)}",
|
|
193
|
+
exc_info=True
|
|
194
|
+
)
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
logger.info(
|
|
198
|
+
f"通道池初始化完成 - 连接: {self._current_host}:{self.port}(版本: {self._connection_version}), "
|
|
199
|
+
f"可用通道数: {len(self._free_channels)}/{self.channel_pool_size}(均带自动恢复)"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def _reconnect_if_needed(self) -> bool:
|
|
203
|
+
"""连接失效时重连(加锁保护,严格单连接+释放旧资源)"""
|
|
204
|
+
# 快速判断,避免无效加锁
|
|
205
|
+
if self._is_shutdown or self._reconnecting:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
self._reconnecting = True
|
|
209
|
+
try:
|
|
210
|
+
logger.warning(f"连接失效(当前版本: {self._connection_version}),开始重连...")
|
|
211
|
+
|
|
212
|
+
# 1. 强制释放所有旧资源(加锁保证原子性)
|
|
213
|
+
await self._safe_close_old_resources()
|
|
214
|
+
|
|
215
|
+
# 2. 递增连接版本号(加锁保证原子性,区分新旧连接)
|
|
216
|
+
async with self._lock:
|
|
217
|
+
self._connection_version += 1
|
|
218
|
+
target_version = self._connection_version
|
|
219
|
+
|
|
220
|
+
# 3. 创建新连接(保留原生自动恢复)
|
|
221
|
+
new_conn = await self._create_single_connection()
|
|
222
|
+
|
|
223
|
+
# 4. 绑定新连接(加锁保证原子性)
|
|
224
|
+
async with self._lock:
|
|
225
|
+
self._connection = new_conn
|
|
226
|
+
|
|
227
|
+
# 5. 重新初始化通道池(新通道带自动恢复)
|
|
228
|
+
await self._init_channel_pool()
|
|
229
|
+
|
|
230
|
+
# 6. 恢复消费者通道(新通道带自动恢复)
|
|
231
|
+
await self._restore_consumer_channels()
|
|
232
|
+
|
|
233
|
+
logger.info(f"重连成功(新连接版本: {target_version}),所有通道均带自动恢复")
|
|
234
|
+
async with self._lock:
|
|
235
|
+
self._initialized = True # 重连成功后标记为已初始化
|
|
236
|
+
return True
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"重连失败: {str(e)}", exc_info=True)
|
|
239
|
+
async with self._lock:
|
|
240
|
+
self._initialized = False
|
|
241
|
+
return False
|
|
242
|
+
finally:
|
|
243
|
+
self._reconnecting = False
|
|
244
|
+
|
|
245
|
+
async def _restore_consumer_channels(self):
|
|
246
|
+
"""重连后恢复消费者通道(加锁保证原子性,新通道带自动恢复)"""
|
|
247
|
+
async with self._lock:
|
|
248
|
+
if not self._consumer_channels or not self._connection or self._connection.is_closed:
|
|
249
|
+
return
|
|
250
|
+
logger.info(
|
|
251
|
+
f"开始恢复 {len(self._consumer_channels)} 个消费者通道(连接版本: {self._connection_version})")
|
|
252
|
+
|
|
253
|
+
# 临时保存消费者配置(队列名、回调、auto_ack、参数)
|
|
254
|
+
consumer_configs = list(self._consumer_channels.items())
|
|
255
|
+
self._consumer_channels.clear()
|
|
256
|
+
|
|
257
|
+
# 重新创建消费者(不加锁,避免阻塞其他操作)
|
|
258
|
+
for queue_name, (_, _, callback, auto_ack, kwargs) in consumer_configs:
|
|
259
|
+
try:
|
|
260
|
+
await self.consume_queue(queue_name, callback, auto_ack, **kwargs)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(
|
|
263
|
+
f"恢复消费者队列 {queue_name} 失败: {str(e)}", exc_info=True)
|
|
264
|
+
|
|
265
|
+
async def _clean_invalid_channels(self):
|
|
266
|
+
"""清理失效通道并补充(加锁保证原子性,仅处理当前连接)"""
|
|
267
|
+
async with self._lock:
|
|
268
|
+
if self._is_shutdown or self._reconnecting:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# 1. 校验当前连接有效性
|
|
272
|
+
current_valid = (
|
|
273
|
+
self._connection is not None
|
|
274
|
+
and not self._connection.is_closed
|
|
275
|
+
and not self._reconnecting
|
|
276
|
+
)
|
|
277
|
+
if not current_valid:
|
|
278
|
+
# 连接失效,触发重连(不加锁,避免死锁)
|
|
279
|
+
asyncio.create_task(self._reconnect_if_needed())
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
# 2. 清理空闲通道(仅保留当前连接的有效通道)
|
|
283
|
+
valid_free = []
|
|
284
|
+
for chan in self._free_channels:
|
|
285
|
+
try:
|
|
286
|
+
if not chan.is_closed and chan.connection == self._connection:
|
|
287
|
+
valid_free.append(chan)
|
|
288
|
+
else:
|
|
289
|
+
logger.warning(f"清理失效空闲通道(连接版本不匹配或已关闭)")
|
|
290
|
+
except Exception:
|
|
291
|
+
logger.warning(f"清理异常空闲通道")
|
|
292
|
+
self._free_channels = valid_free
|
|
293
|
+
|
|
294
|
+
# 3. 清理使用中通道(仅保留当前连接的有效通道)
|
|
295
|
+
valid_used = set()
|
|
296
|
+
for chan in self._used_channels:
|
|
297
|
+
try:
|
|
298
|
+
if not chan.is_closed and chan.connection == self._connection:
|
|
299
|
+
valid_used.add(chan)
|
|
300
|
+
else:
|
|
301
|
+
logger.warning(f"清理失效使用中通道(连接版本不匹配或已关闭)")
|
|
302
|
+
except Exception:
|
|
303
|
+
logger.warning(f"清理异常使用中通道")
|
|
304
|
+
self._used_channels = valid_used
|
|
305
|
+
|
|
306
|
+
# 4. 补充通道到指定大小(新通道带自动恢复)
|
|
307
|
+
total_valid = len(self._free_channels) + len(self._used_channels)
|
|
308
|
+
missing = self.channel_pool_size - total_valid
|
|
309
|
+
if missing > 0:
|
|
310
|
+
logger.info(
|
|
311
|
+
f"通道池缺少{missing}个通道,补充中(连接版本: {self._connection_version})...")
|
|
312
|
+
for _ in range(missing):
|
|
313
|
+
try:
|
|
314
|
+
channel = await self._connection.channel() # 新通道带自动恢复
|
|
315
|
+
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
316
|
+
self._free_channels.append(channel)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.error(f"补充通道失败: {str(e)}", exc_info=True)
|
|
319
|
+
break
|
|
320
|
+
|
|
321
|
+
async def init_pools(self):
|
|
322
|
+
"""初始化通道池(加锁保证原子性,仅执行一次)"""
|
|
323
|
+
async with self._lock:
|
|
324
|
+
if self._initialized:
|
|
325
|
+
logger.warning("通道池已初始化,无需重复调用")
|
|
326
|
+
return
|
|
327
|
+
if self._is_shutdown:
|
|
328
|
+
raise RuntimeError("通道池已关闭,无法初始化")
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
# 1. 创建新连接(保留原生自动恢复)
|
|
332
|
+
new_conn = await self._create_single_connection()
|
|
333
|
+
|
|
334
|
+
# 2. 初始化连接版本号和绑定连接(加锁保证原子性)
|
|
335
|
+
async with self._lock:
|
|
336
|
+
self._connection_version += 1
|
|
337
|
+
self._connection = new_conn
|
|
338
|
+
|
|
339
|
+
# 3. 初始化通道池(新通道带自动恢复)
|
|
340
|
+
await self._init_channel_pool()
|
|
341
|
+
|
|
342
|
+
# 4. 标记为已初始化(加锁保证原子性)
|
|
343
|
+
async with self._lock:
|
|
344
|
+
self._initialized = True
|
|
345
|
+
|
|
346
|
+
logger.info("RabbitMQ单连接通道池初始化完成(所有通道均带自动恢复)")
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.error(f"初始化失败: {str(e)}", exc_info=True)
|
|
349
|
+
await self._safe_close_old_resources()
|
|
350
|
+
raise
|
|
351
|
+
|
|
352
|
+
async def acquire_channel(self) -> Tuple[Channel, AbstractRobustConnection]:
|
|
353
|
+
"""获取通道(加锁保证原子性,返回当前连接+带自动恢复的通道)"""
|
|
354
|
+
# 快速判断,避免无效加锁
|
|
355
|
+
async with self._lock:
|
|
356
|
+
if not self._initialized:
|
|
357
|
+
raise RuntimeError("通道池未初始化,请先调用init_pools()")
|
|
358
|
+
if self._is_shutdown:
|
|
359
|
+
raise RuntimeError("通道池已关闭,无法获取通道")
|
|
360
|
+
|
|
361
|
+
# 先清理失效通道(加锁保证原子性)
|
|
362
|
+
await self._clean_invalid_channels()
|
|
363
|
+
|
|
364
|
+
async with self._lock:
|
|
365
|
+
# 双重校验连接有效性
|
|
366
|
+
current_valid = (
|
|
367
|
+
self._connection is not None
|
|
368
|
+
and not self._connection.is_closed
|
|
369
|
+
and not self._reconnecting
|
|
370
|
+
)
|
|
371
|
+
if not current_valid:
|
|
372
|
+
# 连接失效,触发重连(不加锁,避免死锁)
|
|
373
|
+
reconnect_success = await self._reconnect_if_needed()
|
|
374
|
+
if not reconnect_success:
|
|
375
|
+
raise RuntimeError("连接失效且重连失败,无法获取通道")
|
|
376
|
+
|
|
377
|
+
# 优先从空闲池获取(带自动恢复的通道)
|
|
378
|
+
if self._free_channels:
|
|
379
|
+
channel = self._free_channels.pop()
|
|
380
|
+
self._used_channels.add(channel)
|
|
381
|
+
return channel, self._connection
|
|
382
|
+
|
|
383
|
+
# 通道池已满,创建临时通道(带自动恢复,用完关闭)
|
|
384
|
+
try:
|
|
385
|
+
channel = await self._connection.channel() # 临时通道带自动恢复
|
|
386
|
+
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
387
|
+
self._used_channels.add(channel)
|
|
388
|
+
logger.warning(
|
|
389
|
+
f"通道池已达上限({self.channel_pool_size}),创建临时通道(带自动恢复,用完自动关闭)"
|
|
390
|
+
)
|
|
391
|
+
return channel, self._connection
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.error(f"获取通道失败: {str(e)}", exc_info=True)
|
|
394
|
+
raise
|
|
395
|
+
|
|
396
|
+
async def release_channel(self, channel: Channel, conn: AbstractRobustConnection):
|
|
397
|
+
"""释放通道(加锁保证原子性,仅归还当前连接的有效通道)"""
|
|
398
|
+
# 快速判断,避免无效加锁
|
|
399
|
+
if not channel or not conn or self._is_shutdown:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
async with self._lock:
|
|
403
|
+
# 仅处理当前连接的通道(旧连接的通道直接关闭)
|
|
404
|
+
if conn != self._connection:
|
|
405
|
+
try:
|
|
406
|
+
if not channel.is_closed:
|
|
407
|
+
await channel.close()
|
|
408
|
+
logger.warning(f"已关闭非当前连接的通道(版本不匹配,自动恢复终止)")
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.warning(f"关闭非当前连接通道失败: {str(e)}")
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# 通道不在使用中,直接返回
|
|
414
|
+
if channel not in self._used_channels:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# 移除使用中标记
|
|
418
|
+
self._used_channels.remove(channel)
|
|
419
|
+
|
|
420
|
+
# 仅归还有效通道(当前连接有效+通道未关闭+池未满)
|
|
421
|
+
current_valid = (
|
|
422
|
+
self._connection is not None
|
|
423
|
+
and not self._connection.is_closed
|
|
424
|
+
and not self._reconnecting
|
|
425
|
+
)
|
|
426
|
+
if current_valid and not channel.is_closed and len(self._free_channels) < self.channel_pool_size:
|
|
427
|
+
self._free_channels.append(channel)
|
|
428
|
+
else:
|
|
429
|
+
# 无效通道直接关闭(终止自动恢复)
|
|
430
|
+
try:
|
|
431
|
+
if not channel.is_closed:
|
|
432
|
+
await channel.close()
|
|
433
|
+
except Exception as e:
|
|
434
|
+
logger.warning(f"关闭通道失败: {str(e)}")
|
|
435
|
+
|
|
436
|
+
async def declare_queue(self, queue_name: str, **kwargs) -> AbstractQueue:
|
|
437
|
+
"""声明队列(使用池内通道,带自动恢复)"""
|
|
438
|
+
channel, conn = await self.acquire_channel()
|
|
439
|
+
try:
|
|
440
|
+
return await channel.declare_queue(queue_name, **kwargs)
|
|
441
|
+
finally:
|
|
442
|
+
await self.release_channel(channel, conn)
|
|
443
|
+
|
|
444
|
+
async def declare_exchange(self, exchange_name: str, exchange_type: str = "direct", **kwargs) -> AbstractExchange:
|
|
445
|
+
"""声明交换机(使用池内通道,带自动恢复)"""
|
|
446
|
+
channel, conn = await self.acquire_channel()
|
|
447
|
+
try:
|
|
448
|
+
return await channel.declare_exchange(exchange_name, exchange_type, **kwargs)
|
|
449
|
+
finally:
|
|
450
|
+
await self.release_channel(channel, conn)
|
|
451
|
+
|
|
452
|
+
async def publish_message(self, routing_key: str, message_body: bytes, exchange_name: str = "", **kwargs):
|
|
453
|
+
"""发布消息(使用池内通道,带自动恢复)"""
|
|
454
|
+
channel, conn = await self.acquire_channel()
|
|
455
|
+
try:
|
|
456
|
+
exchange = channel.default_exchange if not exchange_name else await channel.get_exchange(exchange_name)
|
|
457
|
+
message = Message(body=message_body, **kwargs)
|
|
458
|
+
await exchange.publish(message, routing_key=routing_key)
|
|
459
|
+
logger.debug(
|
|
460
|
+
f"消息发布成功 - 连接: {self._current_host}:{self.port}(版本: {self._connection_version}), "
|
|
461
|
+
f"交换机: {exchange.name}, 路由键: {routing_key}"
|
|
462
|
+
)
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.error(f"发布消息失败: {str(e)}", exc_info=True)
|
|
465
|
+
# 发布失败触发重连(下次使用新通道)
|
|
466
|
+
asyncio.create_task(self._reconnect_if_needed())
|
|
467
|
+
raise
|
|
468
|
+
finally:
|
|
469
|
+
await self.release_channel(channel, conn)
|
|
470
|
+
|
|
471
|
+
async def consume_queue(self, queue_name: str, callback: Callable[[AbstractMessage], asyncio.Future], auto_ack: bool = False, **kwargs):
|
|
472
|
+
"""消费队列(使用独立通道,带自动恢复,支持多消费者)"""
|
|
473
|
+
# 快速判断,避免无效加锁
|
|
474
|
+
async with self._lock:
|
|
475
|
+
if not self._initialized:
|
|
476
|
+
raise RuntimeError("通道池未初始化,请先调用init_pools()")
|
|
477
|
+
if self._is_shutdown:
|
|
478
|
+
raise RuntimeError("通道池已关闭,无法启动消费")
|
|
479
|
+
|
|
480
|
+
# 先声明队列(确保队列存在)
|
|
481
|
+
await self.declare_queue(queue_name, **kwargs)
|
|
482
|
+
|
|
483
|
+
# 获取独立通道(消费者通道不放入普通池)
|
|
484
|
+
channel, conn = await self.acquire_channel()
|
|
485
|
+
|
|
486
|
+
# 注册消费者通道(加锁保证原子性)
|
|
487
|
+
async with self._lock:
|
|
488
|
+
self._consumer_channels[queue_name] = (
|
|
489
|
+
channel, conn, callback, auto_ack, kwargs)
|
|
490
|
+
|
|
491
|
+
async def consume_callback_wrapper(message: AbstractMessage):
|
|
492
|
+
"""消费回调包装(处理通道失效和自动恢复)"""
|
|
493
|
+
try:
|
|
494
|
+
# 加锁校验通道和连接状态(原子性)
|
|
495
|
+
async with self._lock:
|
|
496
|
+
conn_matched = conn == self._connection
|
|
497
|
+
channel_valid = not channel.is_closed
|
|
498
|
+
current_conn_valid = (
|
|
499
|
+
self._connection is not None
|
|
500
|
+
and not self._connection.is_closed
|
|
501
|
+
and not self._reconnecting
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# 通道/连接失效,触发重连恢复
|
|
505
|
+
if not conn_matched or not channel_valid or not current_conn_valid:
|
|
506
|
+
logger.warning(
|
|
507
|
+
f"消费者通道 {queue_name} 失效(连接版本不匹配/通道关闭),触发重连恢复")
|
|
508
|
+
|
|
509
|
+
# 移除旧消费者记录(加锁保证原子性)
|
|
510
|
+
async with self._lock:
|
|
511
|
+
if self._consumer_channels.get(queue_name) == (channel, conn, callback, auto_ack, kwargs):
|
|
512
|
+
del self._consumer_channels[queue_name]
|
|
513
|
+
|
|
514
|
+
# 释放旧通道(加锁保证原子性)
|
|
515
|
+
await self.release_channel(channel, conn)
|
|
516
|
+
|
|
517
|
+
# 重新创建消费者(新通道带自动恢复)
|
|
518
|
+
asyncio.create_task(self.consume_queue(
|
|
519
|
+
queue_name, callback, auto_ack, **kwargs))
|
|
520
|
+
|
|
521
|
+
# Nack消息(避免丢失)
|
|
522
|
+
if not auto_ack:
|
|
523
|
+
await message.nack(requeue=True)
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
# 执行业务回调
|
|
527
|
+
await callback(message)
|
|
528
|
+
if not auto_ack:
|
|
529
|
+
await message.ack()
|
|
530
|
+
except ChannelClosed as e:
|
|
531
|
+
logger.error(f"消费者通道 {queue_name} 关闭: {str(e)}", exc_info=True)
|
|
532
|
+
if not auto_ack:
|
|
533
|
+
await message.nack(requeue=True)
|
|
534
|
+
asyncio.create_task(self._reconnect_if_needed())
|
|
535
|
+
except aiormq.exceptions.ChannelInvalidStateError as e:
|
|
536
|
+
logger.error(
|
|
537
|
+
f"消费者通道 {queue_name} 状态异常: {str(e)}", exc_info=True)
|
|
538
|
+
if not auto_ack:
|
|
539
|
+
await message.nack(requeue=True)
|
|
540
|
+
asyncio.create_task(self.consume_queue(
|
|
541
|
+
queue_name, callback, auto_ack, **kwargs))
|
|
542
|
+
except Exception as e:
|
|
543
|
+
logger.error(
|
|
544
|
+
f"消费消息失败(队列: {queue_name}): {str(e)}", exc_info=True)
|
|
545
|
+
if not auto_ack:
|
|
546
|
+
await message.nack(requeue=True)
|
|
547
|
+
|
|
548
|
+
# 日志输出(加锁获取当前连接信息)
|
|
549
|
+
async with self._lock:
|
|
550
|
+
current_host = self._current_host
|
|
551
|
+
current_version = self._connection_version
|
|
552
|
+
|
|
553
|
+
logger.info(
|
|
554
|
+
f"开始消费队列: {queue_name} - 连接: {current_host}:{self.port}(版本: {current_version}), "
|
|
555
|
+
f"通道带自动恢复"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
# 启动消费(使用带自动恢复的通道)
|
|
560
|
+
await channel.basic_consume(
|
|
561
|
+
queue_name,
|
|
562
|
+
consumer_callback=consume_callback_wrapper,
|
|
563
|
+
auto_ack=auto_ack,
|
|
564
|
+
**kwargs
|
|
565
|
+
)
|
|
566
|
+
except Exception as e:
|
|
567
|
+
logger.error(f"启动消费失败(队列: {queue_name}): {str(e)}", exc_info=True)
|
|
568
|
+
# 清理异常的消费者记录和通道(加锁保证原子性)
|
|
569
|
+
await self.release_channel(channel, conn)
|
|
570
|
+
async with self._lock:
|
|
571
|
+
if self._consumer_channels.get(queue_name) == (channel, conn, callback, auto_ack, kwargs):
|
|
572
|
+
del self._consumer_channels[queue_name]
|
|
573
|
+
raise
|
|
574
|
+
|
|
575
|
+
async def close(self):
|
|
576
|
+
"""关闭通道池(加锁保证原子性,释放所有资源)"""
|
|
577
|
+
async with self._lock:
|
|
578
|
+
if self._is_shutdown:
|
|
579
|
+
logger.warning("通道池已关闭,无需重复操作")
|
|
580
|
+
return
|
|
581
|
+
self._is_shutdown = True
|
|
582
|
+
|
|
583
|
+
logger.info("开始关闭RabbitMQ单连接通道池...")
|
|
584
|
+
# 强制释放所有资源(包括自动恢复的通道)
|
|
585
|
+
await self._safe_close_old_resources()
|
|
586
|
+
logger.info("RabbitMQ单连接通道池已完全关闭(所有自动恢复逻辑已终止)")
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/rabbitmq/rabbitmq_service.py
RENAMED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
from typing import (
|
|
4
|
-
Callable, Coroutine, Dict, List, Optional, Type, Union, Any, Set
|
|
4
|
+
Callable, Coroutine, Dict, List, Optional, Type, Union, Any, Set
|
|
5
5
|
)
|
|
6
6
|
from pydantic import BaseModel
|
|
7
|
-
from aio_pika.abc import AbstractIncomingMessage, ConsumerTag
|
|
8
|
-
from aio_pika import Channel, exceptions as aio_pika_exceptions
|
|
7
|
+
from aio_pika.abc import AbstractIncomingMessage, ConsumerTag
|
|
9
8
|
|
|
10
9
|
from sycommon.models.mqmsg_model import MQMsgModel
|
|
11
10
|
from sycommon.models.mqlistener_config import RabbitMQListenerConfig
|
|
@@ -111,6 +110,8 @@ class RabbitMQService:
|
|
|
111
110
|
if not hosts_list:
|
|
112
111
|
raise ValueError("RabbitMQ集群配置为空,请检查host参数")
|
|
113
112
|
|
|
113
|
+
global_prefetch_count = cls._config.get('prefetch_count', 2)
|
|
114
|
+
|
|
114
115
|
# 创建连接池
|
|
115
116
|
cls._connection_pool = RabbitMQConnectionPool(
|
|
116
117
|
hosts=hosts_list,
|
|
@@ -118,7 +119,8 @@ class RabbitMQService:
|
|
|
118
119
|
username=cls._config.get('username', ""),
|
|
119
120
|
password=cls._config.get('password', ""),
|
|
120
121
|
virtualhost=cls._config.get('virtual-host', "/"),
|
|
121
|
-
app_name=cls._config.get("APP_NAME", "")
|
|
122
|
+
app_name=cls._config.get("APP_NAME", ""),
|
|
123
|
+
prefetch_count=global_prefetch_count,
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
# 初始化连接池
|
|
@@ -174,7 +176,7 @@ class RabbitMQService:
|
|
|
174
176
|
f"监控客户端 '{client_name}' 连接状态失败: {str(e)}", exc_info=True)
|
|
175
177
|
|
|
176
178
|
# 检查连接池状态(如果连接池已关闭,重新初始化)
|
|
177
|
-
if not cls._connection_pool.is_alive:
|
|
179
|
+
if not await cls._connection_pool.is_alive:
|
|
178
180
|
logger.error("RabbitMQ连接池已关闭,尝试重新初始化")
|
|
179
181
|
asyncio.create_task(cls._init_connection_pool())
|
|
180
182
|
|
|
@@ -487,9 +489,11 @@ class RabbitMQService:
|
|
|
487
489
|
# 转换配置并强制设置create_if_not_exists为True
|
|
488
490
|
listener_dict = listener_config.model_dump()
|
|
489
491
|
listener_dict['create_if_not_exists'] = True
|
|
492
|
+
listener_dict['prefetch_count'] = listener_config.prefetch_count
|
|
490
493
|
queue_name = listener_dict['queue_name']
|
|
491
494
|
|
|
492
|
-
logger.info(
|
|
495
|
+
logger.info(
|
|
496
|
+
f"设置监听器 {idx+1}/{len(listeners)}: {queue_name} (prefetch_count: {listener_config.prefetch_count})")
|
|
493
497
|
|
|
494
498
|
# 添加监听器
|
|
495
499
|
await cls.add_listener(**listener_dict)
|
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Optional, List, Set, Iterator, Tuple
|
|
3
|
-
from aio_pika import connect_robust, Channel, Message
|
|
4
|
-
from aio_pika.abc import (
|
|
5
|
-
AbstractRobustConnection, AbstractQueue, AbstractExchange, AbstractMessage
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
from sycommon.logging.kafka_log import SYLogger
|
|
9
|
-
|
|
10
|
-
logger = SYLogger
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class RabbitMQConnectionPool:
|
|
14
|
-
"""单连接RabbitMQ通道池(严格单连接)"""
|
|
15
|
-
|
|
16
|
-
def __init__(
|
|
17
|
-
self,
|
|
18
|
-
hosts: List[str],
|
|
19
|
-
port: int,
|
|
20
|
-
username: str,
|
|
21
|
-
password: str,
|
|
22
|
-
virtualhost: str = "/",
|
|
23
|
-
channel_pool_size: int = 1,
|
|
24
|
-
heartbeat: int = 30,
|
|
25
|
-
app_name: str = "",
|
|
26
|
-
connection_timeout: int = 30,
|
|
27
|
-
reconnect_interval: int = 30,
|
|
28
|
-
prefetch_count: int = 2,
|
|
29
|
-
):
|
|
30
|
-
self.hosts = [host.strip() for host in hosts if host.strip()]
|
|
31
|
-
if not self.hosts:
|
|
32
|
-
raise ValueError("至少需要提供一个RabbitMQ主机地址")
|
|
33
|
-
|
|
34
|
-
# 连接配置(所有通道共享此连接的配置)
|
|
35
|
-
self.port = port
|
|
36
|
-
self.username = username
|
|
37
|
-
self.password = password
|
|
38
|
-
self.virtualhost = virtualhost
|
|
39
|
-
self.app_name = app_name or "rabbitmq-client"
|
|
40
|
-
self.heartbeat = heartbeat
|
|
41
|
-
self.connection_timeout = connection_timeout
|
|
42
|
-
self.reconnect_interval = reconnect_interval
|
|
43
|
-
self.prefetch_count = prefetch_count
|
|
44
|
-
self.channel_pool_size = channel_pool_size
|
|
45
|
-
|
|
46
|
-
# 节点轮询:仅用于连接失效时切换节点(仍保持单连接)
|
|
47
|
-
self._host_iterator: Iterator[str] = self._create_host_iterator()
|
|
48
|
-
self._current_host: Optional[str] = None # 当前连接的节点
|
|
49
|
-
|
|
50
|
-
# 核心资源(严格单连接 + 通道池)
|
|
51
|
-
self._connection: Optional[AbstractRobustConnection] = None # 唯一连接
|
|
52
|
-
self._free_channels: List[Channel] = [] # 通道池(仅存储当前连接的通道)
|
|
53
|
-
self._used_channels: Set[Channel] = set()
|
|
54
|
-
|
|
55
|
-
# 状态控制(确保线程安全)
|
|
56
|
-
self._lock = asyncio.Lock()
|
|
57
|
-
self._initialized = False
|
|
58
|
-
self._is_shutdown = False
|
|
59
|
-
self._reconnecting = False # 避免重连并发冲突
|
|
60
|
-
|
|
61
|
-
def _create_host_iterator(self) -> Iterator[str]:
|
|
62
|
-
"""创建节点轮询迭代器(无限循环,仅用于切换节点)"""
|
|
63
|
-
while True:
|
|
64
|
-
for host in self.hosts:
|
|
65
|
-
yield host
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def is_alive(self) -> bool:
|
|
69
|
-
"""检查唯一连接是否存活(使用is_closed判断,兼容所有版本)"""
|
|
70
|
-
if not self._initialized or not self._connection:
|
|
71
|
-
return False
|
|
72
|
-
# 异步清理失效通道(不影响主流程)
|
|
73
|
-
asyncio.create_task(self._clean_invalid_channels())
|
|
74
|
-
return not self._connection.is_closed
|
|
75
|
-
|
|
76
|
-
async def _safe_close_resources(self):
|
|
77
|
-
"""安全关闭资源:先关通道,再关连接(保证单连接特性)"""
|
|
78
|
-
async with self._lock:
|
|
79
|
-
# 1. 关闭所有通道(无论空闲还是使用中)
|
|
80
|
-
all_channels = self._free_channels + list(self._used_channels)
|
|
81
|
-
for channel in all_channels:
|
|
82
|
-
try:
|
|
83
|
-
if not channel.is_closed:
|
|
84
|
-
await channel.close()
|
|
85
|
-
except Exception as e:
|
|
86
|
-
logger.warning(f"关闭通道失败: {str(e)}")
|
|
87
|
-
self._free_channels.clear()
|
|
88
|
-
self._used_channels.clear()
|
|
89
|
-
|
|
90
|
-
# 2. 关闭唯一连接
|
|
91
|
-
if self._connection:
|
|
92
|
-
try:
|
|
93
|
-
if not self._connection.is_closed:
|
|
94
|
-
await self._connection.close()
|
|
95
|
-
logger.info(f"已关闭唯一连接: {self._current_host}:{self.port}")
|
|
96
|
-
except Exception as e:
|
|
97
|
-
logger.warning(f"关闭连接失败: {str(e)}")
|
|
98
|
-
self._connection = None # 置空,确保单连接
|
|
99
|
-
|
|
100
|
-
async def _create_single_connection(self) -> AbstractRobustConnection:
|
|
101
|
-
"""创建唯一连接(失败时轮询节点,切换前关闭旧连接)"""
|
|
102
|
-
max_attempts = len(self.hosts) # 每个节点尝试1次
|
|
103
|
-
attempts = 0
|
|
104
|
-
last_error: Optional[Exception] = None
|
|
105
|
-
|
|
106
|
-
while attempts < max_attempts and not self._is_shutdown:
|
|
107
|
-
next_host = next(self._host_iterator)
|
|
108
|
-
|
|
109
|
-
# 切换节点前:强制关闭旧连接(保证单连接)
|
|
110
|
-
if self._connection:
|
|
111
|
-
await self._safe_close_resources()
|
|
112
|
-
|
|
113
|
-
self._current_host = next_host
|
|
114
|
-
conn_url = f"amqp://{self.username}:{self.password}@{self._current_host}:{self.port}/{self.virtualhost}"
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
logger.info(f"尝试创建唯一连接: {self._current_host}:{self.port}")
|
|
118
|
-
conn = await connect_robust(
|
|
119
|
-
conn_url,
|
|
120
|
-
properties={
|
|
121
|
-
"connection_name": f"{self.app_name}_single_conn",
|
|
122
|
-
"product": self.app_name
|
|
123
|
-
},
|
|
124
|
-
heartbeat=self.heartbeat,
|
|
125
|
-
timeout=self.connection_timeout,
|
|
126
|
-
reconnect_interval=self.reconnect_interval,
|
|
127
|
-
max_reconnect_attempts=None, # 单节点内部自动重连
|
|
128
|
-
)
|
|
129
|
-
logger.info(f"唯一连接创建成功: {self._current_host}:{self.port}")
|
|
130
|
-
return conn
|
|
131
|
-
except Exception as e:
|
|
132
|
-
attempts += 1
|
|
133
|
-
last_error = e
|
|
134
|
-
logger.error(
|
|
135
|
-
f"连接节点 {self._current_host}:{self.port} 失败({attempts}/{max_attempts}): {str(e)}",
|
|
136
|
-
exc_info=True
|
|
137
|
-
)
|
|
138
|
-
await asyncio.sleep(30) # 避免频繁重试
|
|
139
|
-
|
|
140
|
-
raise ConnectionError(
|
|
141
|
-
f"所有节点创建唯一连接失败(节点列表: {self.hosts})"
|
|
142
|
-
) from last_error
|
|
143
|
-
|
|
144
|
-
async def _init_channel_pool(self):
|
|
145
|
-
"""初始化通道池(绑定到唯一连接,仅创建指定数量的通道)"""
|
|
146
|
-
if not self._connection or self._connection.is_closed:
|
|
147
|
-
raise RuntimeError("无有效连接,无法初始化通道池")
|
|
148
|
-
|
|
149
|
-
async with self._lock:
|
|
150
|
-
self._free_channels.clear()
|
|
151
|
-
self._used_channels.clear()
|
|
152
|
-
|
|
153
|
-
# 创建指定数量的通道(池大小由channel_pool_size控制)
|
|
154
|
-
for i in range(self.channel_pool_size):
|
|
155
|
-
try:
|
|
156
|
-
channel = await self._connection.channel()
|
|
157
|
-
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
158
|
-
self._free_channels.append(channel)
|
|
159
|
-
except Exception as e:
|
|
160
|
-
logger.error(f"创建通道失败(第{i+1}个): {str(e)}", exc_info=True)
|
|
161
|
-
# 通道创建失败不中断,继续创建剩余通道
|
|
162
|
-
continue
|
|
163
|
-
|
|
164
|
-
logger.info(
|
|
165
|
-
f"通道池初始化完成 - 连接: {self._current_host}:{self.port}, "
|
|
166
|
-
f"可用通道数: {len(self._free_channels)}/{self.channel_pool_size}"
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
async def _reconnect_if_needed(self) -> bool:
|
|
170
|
-
"""连接失效时重连(保证单连接)"""
|
|
171
|
-
if self._is_shutdown or self._reconnecting:
|
|
172
|
-
return False
|
|
173
|
-
|
|
174
|
-
self._reconnecting = True
|
|
175
|
-
try:
|
|
176
|
-
logger.warning("连接失效,开始重连...")
|
|
177
|
-
# 重新创建唯一连接
|
|
178
|
-
self._connection = await self._create_single_connection()
|
|
179
|
-
# 重新初始化通道池
|
|
180
|
-
await self._init_channel_pool()
|
|
181
|
-
logger.info("重连成功,通道池已恢复")
|
|
182
|
-
return True
|
|
183
|
-
except Exception as e:
|
|
184
|
-
logger.error(f"重连失败: {str(e)}", exc_info=True)
|
|
185
|
-
self._initialized = False # 重连失败后标记未初始化
|
|
186
|
-
return False
|
|
187
|
-
finally:
|
|
188
|
-
self._reconnecting = False
|
|
189
|
-
|
|
190
|
-
async def _clean_invalid_channels(self):
|
|
191
|
-
"""清理失效通道并补充(仅针对当前唯一连接)"""
|
|
192
|
-
if not self._connection:
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
async with self._lock:
|
|
196
|
-
# 1. 清理空闲通道中的失效通道
|
|
197
|
-
valid_free = [
|
|
198
|
-
chan for chan in self._free_channels if not chan.is_closed]
|
|
199
|
-
invalid_count = len(self._free_channels) - len(valid_free)
|
|
200
|
-
if invalid_count > 0:
|
|
201
|
-
logger.warning(f"清理{invalid_count}个失效空闲通道")
|
|
202
|
-
self._free_channels = valid_free
|
|
203
|
-
|
|
204
|
-
# 2. 清理使用中通道中的失效通道
|
|
205
|
-
valid_used = {
|
|
206
|
-
chan for chan in self._used_channels if not chan.is_closed}
|
|
207
|
-
invalid_used_count = len(self._used_channels) - len(valid_used)
|
|
208
|
-
if invalid_used_count > 0:
|
|
209
|
-
logger.warning(f"清理{invalid_used_count}个失效使用中通道")
|
|
210
|
-
self._used_channels = valid_used
|
|
211
|
-
|
|
212
|
-
# 3. 检查连接是否有效,无效则触发重连
|
|
213
|
-
if self._connection.is_closed:
|
|
214
|
-
await self._reconnect_if_needed()
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
# 4. 补充通道到指定大小(仅使用当前唯一连接创建)
|
|
218
|
-
total_valid = len(self._free_channels) + len(self._used_channels)
|
|
219
|
-
missing = self.channel_pool_size - total_valid
|
|
220
|
-
if missing > 0:
|
|
221
|
-
logger.info(f"通道池缺少{missing}个通道,补充中...")
|
|
222
|
-
for _ in range(missing):
|
|
223
|
-
try:
|
|
224
|
-
channel = await self._connection.channel()
|
|
225
|
-
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
226
|
-
self._free_channels.append(channel)
|
|
227
|
-
except Exception as e:
|
|
228
|
-
logger.error(f"补充通道失败: {str(e)}", exc_info=True)
|
|
229
|
-
break
|
|
230
|
-
|
|
231
|
-
async def init_pools(self):
|
|
232
|
-
"""初始化:创建唯一连接 + 初始化通道池(仅执行一次)"""
|
|
233
|
-
if self._initialized:
|
|
234
|
-
logger.warning("通道池已初始化,无需重复调用")
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
if self._is_shutdown:
|
|
238
|
-
raise RuntimeError("通道池已关闭,无法初始化")
|
|
239
|
-
|
|
240
|
-
try:
|
|
241
|
-
# 1. 创建唯一连接
|
|
242
|
-
self._connection = await self._create_single_connection()
|
|
243
|
-
# 2. 初始化通道池(绑定到该连接)
|
|
244
|
-
await self._init_channel_pool()
|
|
245
|
-
self._initialized = True
|
|
246
|
-
logger.info("RabbitMQ单连接通道池初始化完成")
|
|
247
|
-
except Exception as e:
|
|
248
|
-
logger.error(f"初始化失败: {str(e)}", exc_info=True)
|
|
249
|
-
await self._safe_close_resources()
|
|
250
|
-
raise
|
|
251
|
-
|
|
252
|
-
async def acquire_channel(self) -> Tuple[Channel, AbstractRobustConnection]:
|
|
253
|
-
"""获取通道(返回元组:(通道, 唯一连接),兼容上层代码)"""
|
|
254
|
-
if not self._initialized:
|
|
255
|
-
raise RuntimeError("通道池未初始化,请先调用init_pools()")
|
|
256
|
-
|
|
257
|
-
if self._is_shutdown:
|
|
258
|
-
raise RuntimeError("通道池已关闭,无法获取通道")
|
|
259
|
-
|
|
260
|
-
# 先清理失效通道,确保池内通道有效
|
|
261
|
-
await self._clean_invalid_channels()
|
|
262
|
-
|
|
263
|
-
async with self._lock:
|
|
264
|
-
# 优先从空闲池获取
|
|
265
|
-
if self._free_channels:
|
|
266
|
-
channel = self._free_channels.pop()
|
|
267
|
-
self._used_channels.add(channel)
|
|
268
|
-
# 返回(通道, 唯一连接)元组
|
|
269
|
-
return channel, self._connection
|
|
270
|
-
|
|
271
|
-
# 通道池已满,创建临时通道(超出池大小,用完关闭)
|
|
272
|
-
try:
|
|
273
|
-
if not self._connection or self._connection.is_closed:
|
|
274
|
-
raise RuntimeError("唯一连接已失效,无法创建临时通道")
|
|
275
|
-
|
|
276
|
-
channel = await self._connection.channel()
|
|
277
|
-
await channel.set_qos(prefetch_count=self.prefetch_count)
|
|
278
|
-
self._used_channels.add(channel)
|
|
279
|
-
logger.warning(
|
|
280
|
-
f"通道池已达上限({self.channel_pool_size}),创建临时通道(用完自动关闭)"
|
|
281
|
-
)
|
|
282
|
-
# 返回(通道, 唯一连接)元组
|
|
283
|
-
return channel, self._connection
|
|
284
|
-
except Exception as e:
|
|
285
|
-
logger.error(f"获取通道失败: {str(e)}", exc_info=True)
|
|
286
|
-
raise
|
|
287
|
-
|
|
288
|
-
async def release_channel(self, channel: Channel, conn: AbstractRobustConnection):
|
|
289
|
-
"""释放通道(接收通道和连接参数,兼容上层代码)"""
|
|
290
|
-
if not channel or not conn or self._is_shutdown:
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
# 仅处理当前唯一连接的通道(避免无效连接的通道)
|
|
294
|
-
if conn != self._connection:
|
|
295
|
-
try:
|
|
296
|
-
await channel.close()
|
|
297
|
-
logger.warning("已关闭非当前连接的通道(可能是重连后的旧通道)")
|
|
298
|
-
except Exception as e:
|
|
299
|
-
logger.warning(f"关闭非当前连接通道失败: {str(e)}")
|
|
300
|
-
return
|
|
301
|
-
|
|
302
|
-
async with self._lock:
|
|
303
|
-
if channel not in self._used_channels:
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
self._used_channels.remove(channel)
|
|
307
|
-
|
|
308
|
-
# 仅归还:当前连接有效 + 通道未关闭 + 池未满
|
|
309
|
-
if (not self._connection.is_closed
|
|
310
|
-
and not channel.is_closed
|
|
311
|
-
and len(self._free_channels) < self.channel_pool_size):
|
|
312
|
-
self._free_channels.append(channel)
|
|
313
|
-
else:
|
|
314
|
-
# 无效通道直接关闭
|
|
315
|
-
try:
|
|
316
|
-
await channel.close()
|
|
317
|
-
except Exception as e:
|
|
318
|
-
logger.warning(f"关闭通道失败: {str(e)}")
|
|
319
|
-
|
|
320
|
-
async def declare_queue(self, queue_name: str, **kwargs) -> AbstractQueue:
|
|
321
|
-
"""声明队列(使用池内通道,共享唯一连接)"""
|
|
322
|
-
channel, conn = await self.acquire_channel()
|
|
323
|
-
try:
|
|
324
|
-
return await channel.declare_queue(queue_name, **kwargs)
|
|
325
|
-
finally:
|
|
326
|
-
await self.release_channel(channel, conn)
|
|
327
|
-
|
|
328
|
-
async def declare_exchange(self, exchange_name: str, exchange_type: str = "direct", **kwargs) -> AbstractExchange:
|
|
329
|
-
"""声明交换机(使用池内通道,共享唯一连接)"""
|
|
330
|
-
channel, conn = await self.acquire_channel()
|
|
331
|
-
try:
|
|
332
|
-
return await channel.declare_exchange(exchange_name, exchange_type, **kwargs)
|
|
333
|
-
finally:
|
|
334
|
-
await self.release_channel(channel, conn)
|
|
335
|
-
|
|
336
|
-
async def publish_message(self, routing_key: str, message_body: bytes, exchange_name: str = "", **kwargs):
|
|
337
|
-
"""发布消息(使用池内通道,共享唯一连接)"""
|
|
338
|
-
channel, conn = await self.acquire_channel()
|
|
339
|
-
try:
|
|
340
|
-
exchange = channel.default_exchange if not exchange_name else await channel.get_exchange(exchange_name)
|
|
341
|
-
message = Message(body=message_body, **kwargs)
|
|
342
|
-
await exchange.publish(message, routing_key=routing_key)
|
|
343
|
-
logger.debug(
|
|
344
|
-
f"消息发布成功 - 节点: {self._current_host}, 交换机: {exchange.name}, 路由键: {routing_key}"
|
|
345
|
-
)
|
|
346
|
-
except Exception as e:
|
|
347
|
-
logger.error(f"发布消息失败: {str(e)}", exc_info=True)
|
|
348
|
-
raise
|
|
349
|
-
finally:
|
|
350
|
-
await self.release_channel(channel, conn)
|
|
351
|
-
|
|
352
|
-
async def consume_queue(self, queue_name: str, callback, auto_ack: bool = False, **kwargs):
|
|
353
|
-
"""消费队列(使用池内通道,共享唯一连接)"""
|
|
354
|
-
if not self._initialized:
|
|
355
|
-
raise RuntimeError("通道池未初始化,请先调用init_pools()")
|
|
356
|
-
|
|
357
|
-
queue = await self.declare_queue(queue_name, **kwargs)
|
|
358
|
-
current_channel, current_conn = await self.acquire_channel() # 元组解包
|
|
359
|
-
|
|
360
|
-
async def consume_callback_wrapper(message: AbstractMessage):
|
|
361
|
-
"""消费回调包装(处理通道失效重连)"""
|
|
362
|
-
nonlocal current_channel, current_conn
|
|
363
|
-
try:
|
|
364
|
-
# 检查通道是否有效(连接可能已切换)
|
|
365
|
-
if (current_channel.is_closed
|
|
366
|
-
or current_conn.is_closed
|
|
367
|
-
or current_conn != self._connection):
|
|
368
|
-
logger.warning("消费通道失效,重新获取通道...")
|
|
369
|
-
await self.release_channel(current_channel, current_conn)
|
|
370
|
-
current_channel, current_conn = await self.acquire_channel()
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
await callback(message)
|
|
374
|
-
if not auto_ack:
|
|
375
|
-
await message.ack()
|
|
376
|
-
except Exception as e:
|
|
377
|
-
logger.error(f"消费消息失败: {str(e)}", exc_info=True)
|
|
378
|
-
if not auto_ack:
|
|
379
|
-
await message.nack(requeue=True)
|
|
380
|
-
|
|
381
|
-
logger.info(f"开始消费队列: {queue_name}(连接节点: {self._current_host})")
|
|
382
|
-
try:
|
|
383
|
-
async with queue.iterator() as queue_iter:
|
|
384
|
-
async for message in queue_iter:
|
|
385
|
-
if self._is_shutdown:
|
|
386
|
-
logger.info("消费已停止,退出消费循环")
|
|
387
|
-
break
|
|
388
|
-
await consume_callback_wrapper(message)
|
|
389
|
-
finally:
|
|
390
|
-
await self.release_channel(current_channel, current_conn)
|
|
391
|
-
|
|
392
|
-
async def close(self):
|
|
393
|
-
"""关闭通道池:释放所有通道 + 关闭唯一连接"""
|
|
394
|
-
if self._is_shutdown:
|
|
395
|
-
logger.warning("通道池已关闭,无需重复操作")
|
|
396
|
-
return
|
|
397
|
-
|
|
398
|
-
self._is_shutdown = True
|
|
399
|
-
logger.info("开始关闭RabbitMQ单连接通道池...")
|
|
400
|
-
|
|
401
|
-
# 安全释放所有资源
|
|
402
|
-
await self._safe_close_resources()
|
|
403
|
-
|
|
404
|
-
logger.info("RabbitMQ单连接通道池已完全关闭")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/DatabaseConfig.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/EmbeddingConfig.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/config/RerankerConfig.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/database/base_db_service.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/database/database_service.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/health/health_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/logging/logger_wrapper.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/logging/sql_logger.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/__init__.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/exception.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/middleware.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/monitor_memory.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/timeout.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/middleware/traceid.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/mqlistener_config.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/mqmsg_model.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/models/mqsend_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/rabbitmq/rabbitmq_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/feign_client.py
RENAMED
|
File without changes
|
{sycommon_python_lib-0.1.45 → sycommon_python_lib-0.1.46}/src/sycommon/synacos/nacos_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|