sycommon-python-lib 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sycommon/config/EmbeddingConfig.py +0 -1
- sycommon/config/MQConfig.py +15 -0
- sycommon/database/base_db_service.py +30 -0
- sycommon/logging/kafka_log.py +14 -38
- sycommon/middleware/middleware.py +4 -0
- sycommon/middleware/mq.py +9 -0
- sycommon/models/mqlistener_config.py +38 -0
- sycommon/models/mqmsg_model.py +11 -0
- sycommon/models/mqsend_config.py +8 -0
- sycommon/models/sso_user.py +60 -0
- sycommon/rabbitmq/rabbitmq_client.py +721 -0
- sycommon/rabbitmq/rabbitmq_service.py +476 -0
- sycommon/services.py +164 -20
- sycommon/synacos/feign.py +14 -10
- sycommon/synacos/nacos_service.py +252 -188
- {sycommon_python_lib-0.1.1.dist-info → sycommon_python_lib-0.1.2.dist-info}/METADATA +2 -1
- {sycommon_python_lib-0.1.1.dist-info → sycommon_python_lib-0.1.2.dist-info}/RECORD +19 -10
- {sycommon_python_lib-0.1.1.dist-info → sycommon_python_lib-0.1.2.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.1.dist-info → sycommon_python_lib-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
from typing import Any, Callable, Coroutine, Dict, List, Tuple, Union, Optional, Type
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from aio_pika.abc import AbstractIncomingMessage
|
|
6
|
+
|
|
7
|
+
from sycommon.logging.kafka_log import SYLogger
|
|
8
|
+
from sycommon.models.mqmsg_model import MQMsgModel
|
|
9
|
+
from sycommon.models.mqlistener_config import RabbitMQListenerConfig
|
|
10
|
+
from sycommon.models.mqsend_config import RabbitMQSendConfig
|
|
11
|
+
from sycommon.models.sso_user import SsoUser
|
|
12
|
+
from sycommon.rabbitmq.rabbitmq_client import RabbitMQClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RabbitMQService:
|
|
16
|
+
# 保存多个客户端实例
|
|
17
|
+
clients: Dict[str, RabbitMQClient] = {}
|
|
18
|
+
# 保存多个消费者任务
|
|
19
|
+
consumer_tasks: Dict[str, asyncio.Task] = {}
|
|
20
|
+
# 保存消息处理器
|
|
21
|
+
message_handlers: Dict[str, Callable] = {}
|
|
22
|
+
# 保存配置信息
|
|
23
|
+
config: Optional[dict] = None
|
|
24
|
+
# 存储发送客户端的名称(即队列名)
|
|
25
|
+
sender_client_names: List[str] = []
|
|
26
|
+
# 用于控制消费者任务退出的事件
|
|
27
|
+
_consumer_events: Dict[str, asyncio.Event] = {}
|
|
28
|
+
# 存储消费者标签,用于取消消费
|
|
29
|
+
_consumer_tags: Dict[str, str] = {}
|
|
30
|
+
# 跟踪已完成的初始化操作(全局状态)
|
|
31
|
+
_initialized_queues: Dict[str, Dict[str, bool]] = {}
|
|
32
|
+
# 添加异步锁
|
|
33
|
+
_init_lock: Dict[str, asyncio.Lock] = {}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def init(cls, config: dict) -> Type['RabbitMQService']:
|
|
37
|
+
"""初始化RabbitMQ服务,保存配置"""
|
|
38
|
+
from sycommon.synacos.nacos_service import NacosService
|
|
39
|
+
# 获取 common 配置
|
|
40
|
+
cls.config = NacosService(config).share_configs.get(
|
|
41
|
+
"mq.yml", {}).get('spring', {}).get('rabbitmq', {})
|
|
42
|
+
cls.config["APP_NAME"] = config.get("Name", "")
|
|
43
|
+
return cls()
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
async def check_queue_exists(cls, channel, queue_name: str) -> bool:
|
|
47
|
+
"""检查队列是否存在"""
|
|
48
|
+
try:
|
|
49
|
+
await channel.declare_queue(
|
|
50
|
+
name=queue_name,
|
|
51
|
+
passive=True # 被动模式:仅检查队列是否存在
|
|
52
|
+
)
|
|
53
|
+
return True
|
|
54
|
+
except Exception as e:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def create_client(cls, mq_config: dict, queue_name: str, **kwargs):
|
|
59
|
+
"""创建并返回新的RabbitMQClient实例"""
|
|
60
|
+
# 从kwargs中提取是否自动创建队列的参数,默认True
|
|
61
|
+
create_if_not_exists = kwargs.get('create_if_not_exists', True)
|
|
62
|
+
|
|
63
|
+
# 确保只在需要时处理一次队列名称
|
|
64
|
+
processed_queue_name = queue_name
|
|
65
|
+
|
|
66
|
+
# 如果是监听器且需要自动创建队列,确保队列名拼接app-name(只拼接一次)
|
|
67
|
+
if create_if_not_exists and processed_queue_name and 'app_name' in kwargs and kwargs['app_name']:
|
|
68
|
+
app_name = kwargs['app_name']
|
|
69
|
+
# 只在队列名尚未包含app-name时进行拼接
|
|
70
|
+
if not processed_queue_name.endswith(f".{app_name}"):
|
|
71
|
+
processed_queue_name = f"{processed_queue_name}.{app_name}"
|
|
72
|
+
logging.debug(f"监听器队列名称自动拼接app-name: {processed_queue_name}")
|
|
73
|
+
|
|
74
|
+
return RabbitMQClient(
|
|
75
|
+
host=mq_config.get('host', ""),
|
|
76
|
+
port=mq_config.get('port', 0),
|
|
77
|
+
username=mq_config.get('username', ""),
|
|
78
|
+
password=mq_config.get('password', ""),
|
|
79
|
+
virtualhost=mq_config.get('virtual-host', "/"),
|
|
80
|
+
exchange_name=mq_config.get(
|
|
81
|
+
'exchange_name', "system.topic.exchange"),
|
|
82
|
+
exchange_type=kwargs.get('exchange_type', "topic"),
|
|
83
|
+
queue_name=processed_queue_name, # 使用处理后的队列名
|
|
84
|
+
routing_key=kwargs.get(
|
|
85
|
+
'routing_key', f"{processed_queue_name.split('.')[0]}.#" if processed_queue_name else "#"),
|
|
86
|
+
durable=kwargs.get('durable', True),
|
|
87
|
+
auto_delete=kwargs.get('auto_delete', False),
|
|
88
|
+
auto_parse_json=kwargs.get('auto_parse_json', True),
|
|
89
|
+
create_if_not_exists=create_if_not_exists,
|
|
90
|
+
connection_timeout=kwargs.get('connection_timeout', 10),
|
|
91
|
+
rpc_timeout=kwargs.get('rpc_timeout', 5),
|
|
92
|
+
app_name=kwargs.get('app_name', "")
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
async def setup_rabbitmq(
|
|
97
|
+
cls,
|
|
98
|
+
mq_config: dict,
|
|
99
|
+
client_name: str = "default", ** kwargs
|
|
100
|
+
) -> RabbitMQClient:
|
|
101
|
+
"""初始化RabbitMQ客户端并注册到服务中"""
|
|
102
|
+
if client_name not in cls._init_lock:
|
|
103
|
+
cls._init_lock[client_name] = asyncio.Lock()
|
|
104
|
+
|
|
105
|
+
async with cls._init_lock[client_name]:
|
|
106
|
+
if client_name in cls.clients:
|
|
107
|
+
client = cls.clients[client_name]
|
|
108
|
+
if client.is_connected:
|
|
109
|
+
logging.debug(f"客户端 '{client_name}' 已存在且连接有效,直接返回")
|
|
110
|
+
return client
|
|
111
|
+
else:
|
|
112
|
+
logging.debug(f"客户端 '{client_name}' 存在但连接已关闭,重新连接")
|
|
113
|
+
await client.connect()
|
|
114
|
+
return client
|
|
115
|
+
|
|
116
|
+
initial_queue_name = kwargs.pop('queue_name', '')
|
|
117
|
+
create_if_not_exists = kwargs.get('create_if_not_exists', True)
|
|
118
|
+
|
|
119
|
+
# 检查队列是否已初始化
|
|
120
|
+
if initial_queue_name in cls._initialized_queues:
|
|
121
|
+
logging.debug(f"队列 '{initial_queue_name}' 已初始化过,直接创建客户端")
|
|
122
|
+
client = RabbitMQService.create_client(
|
|
123
|
+
mq_config,
|
|
124
|
+
initial_queue_name,
|
|
125
|
+
app_name=cls.config.get("APP_NAME", ""),
|
|
126
|
+
**kwargs
|
|
127
|
+
)
|
|
128
|
+
await client.connect()
|
|
129
|
+
cls.clients[client_name] = client
|
|
130
|
+
return client
|
|
131
|
+
|
|
132
|
+
# 处理create_if_not_exists参数,避免重复传递
|
|
133
|
+
create_if_not_exists = kwargs.pop('create_if_not_exists', True)
|
|
134
|
+
|
|
135
|
+
# 对于监听器,确保create_if_not_exists为True
|
|
136
|
+
# 判断是否为监听器:客户端名称与队列名称相同通常是监听器
|
|
137
|
+
is_listener = client_name == initial_queue_name and initial_queue_name
|
|
138
|
+
if is_listener:
|
|
139
|
+
create_if_not_exists = True
|
|
140
|
+
logging.debug(f"监听器 '{client_name}' 将自动创建队列(如果不存在)")
|
|
141
|
+
|
|
142
|
+
# 创建客户端(这里会处理队列名称,添加app-name)
|
|
143
|
+
client = RabbitMQService.create_client(
|
|
144
|
+
mq_config,
|
|
145
|
+
initial_queue_name,
|
|
146
|
+
app_name=cls.config.get("APP_NAME", ""),
|
|
147
|
+
create_if_not_exists=create_if_not_exists,
|
|
148
|
+
connection_timeout=mq_config.get('connection_timeout', 15),
|
|
149
|
+
rpc_timeout=mq_config.get('rpc_timeout', 5), ** kwargs
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# 连接时不再处理队列名称,避免重复添加app-name
|
|
153
|
+
await client.connect()
|
|
154
|
+
final_queue_name = client.queue_name
|
|
155
|
+
|
|
156
|
+
if final_queue_name not in cls._initialized_queues:
|
|
157
|
+
cls._initialized_queues[final_queue_name] = {
|
|
158
|
+
"declared": True,
|
|
159
|
+
"bound": True
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cls.clients[client_name] = client
|
|
163
|
+
return client
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
async def setup_senders(cls, senders: List[RabbitMQSendConfig]):
|
|
167
|
+
"""设置MQ发送客户端 - 保持不自动创建队列的行为"""
|
|
168
|
+
async def setup_sender_tasks():
|
|
169
|
+
for idx, sender_config in enumerate(senders):
|
|
170
|
+
try:
|
|
171
|
+
if not sender_config.queue_name:
|
|
172
|
+
raise ValueError(f"发送器配置第{idx+1}项缺少queue_name")
|
|
173
|
+
|
|
174
|
+
normalized_name = sender_config.queue_name
|
|
175
|
+
app_name = cls.config.get("APP_NAME", "")
|
|
176
|
+
if app_name and normalized_name.endswith(f".{app_name}"):
|
|
177
|
+
normalized_name = normalized_name[:-
|
|
178
|
+
len(f".{app_name}")]
|
|
179
|
+
|
|
180
|
+
if normalized_name in cls.sender_client_names:
|
|
181
|
+
logging.debug(f"发送客户端 '{normalized_name}' 已存在,跳过重复配置")
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if normalized_name in cls.clients:
|
|
185
|
+
client = cls.clients[normalized_name]
|
|
186
|
+
if not client.is_connected:
|
|
187
|
+
await client.connect()
|
|
188
|
+
else:
|
|
189
|
+
# 发送端明确设置不自动创建队列
|
|
190
|
+
client = await cls.setup_rabbitmq(
|
|
191
|
+
cls.config,
|
|
192
|
+
client_name=normalized_name,
|
|
193
|
+
exchange_type=sender_config.exchange_type,
|
|
194
|
+
durable=sender_config.durable,
|
|
195
|
+
auto_delete=sender_config.auto_delete,
|
|
196
|
+
auto_parse_json=sender_config.auto_parse_json,
|
|
197
|
+
queue_name=sender_config.queue_name,
|
|
198
|
+
create_if_not_exists=False # 发送端不自动创建队列
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if normalized_name not in cls.clients:
|
|
202
|
+
cls.clients[normalized_name] = client
|
|
203
|
+
logging.info(f"客户端 '{normalized_name}' 已添加到客户端列表")
|
|
204
|
+
|
|
205
|
+
if normalized_name not in cls.sender_client_names:
|
|
206
|
+
cls.sender_client_names.append(normalized_name)
|
|
207
|
+
logging.info(f"发送客户端 '{normalized_name}' 初始化成功")
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logging.error(
|
|
211
|
+
f"初始化发送客户端第{idx+1}项失败: {str(e)}", exc_info=True)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
await setup_sender_tasks()
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logging.error(f"设置发送器时发生错误: {str(e)}", exc_info=True)
|
|
217
|
+
raise
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
async def setup_listeners(cls, listeners: List[RabbitMQListenerConfig]):
|
|
221
|
+
"""设置MQ监听器 - 确保自动创建队列"""
|
|
222
|
+
for listener_config in listeners:
|
|
223
|
+
# 将监听器配置转换为字典并添加到监听器
|
|
224
|
+
# 强制设置create_if_not_exists为True
|
|
225
|
+
listener_dict = listener_config.model_dump()
|
|
226
|
+
listener_dict['create_if_not_exists'] = True
|
|
227
|
+
await cls.add_listener(**listener_dict)
|
|
228
|
+
# 启动所有消费者
|
|
229
|
+
await cls.start_all_consumers()
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
async def add_listener(
|
|
233
|
+
cls,
|
|
234
|
+
queue_name: str,
|
|
235
|
+
handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine], ** kwargs
|
|
236
|
+
) -> None:
|
|
237
|
+
"""添加RabbitMQ监听器 - 确保自动创建队列并拼接app-name"""
|
|
238
|
+
if not cls.config:
|
|
239
|
+
raise ValueError("RabbitMQService尚未初始化,请先调用init方法")
|
|
240
|
+
|
|
241
|
+
if queue_name in cls.message_handlers:
|
|
242
|
+
logging.debug(f"监听器 '{queue_name}' 已存在,跳过重复添加")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# 为监听器强制设置create_if_not_exists=True
|
|
246
|
+
kwargs['create_if_not_exists'] = True
|
|
247
|
+
|
|
248
|
+
# 创建并初始化客户端(会处理队列名称)
|
|
249
|
+
await cls.setup_rabbitmq(
|
|
250
|
+
cls.config,
|
|
251
|
+
client_name=queue_name,
|
|
252
|
+
queue_name=queue_name,
|
|
253
|
+
**kwargs
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# 注册消息处理器
|
|
257
|
+
cls.register_handler(queue_name, handler)
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def register_handler(
|
|
261
|
+
cls,
|
|
262
|
+
client_name: str,
|
|
263
|
+
handler: Callable[[MQMsgModel, AbstractIncomingMessage], Coroutine]
|
|
264
|
+
) -> None:
|
|
265
|
+
"""为特定客户端注册消息处理器"""
|
|
266
|
+
cls.message_handlers[client_name] = handler
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
async def start_all_consumers(cls) -> None:
|
|
270
|
+
"""启动所有已注册客户端的消费者"""
|
|
271
|
+
for client_name in cls.clients:
|
|
272
|
+
await cls.start_consumer(client_name)
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
async def start_consumer(cls, client_name: str = "default") -> None:
|
|
276
|
+
"""启动指定客户端的消费者"""
|
|
277
|
+
if client_name in cls.consumer_tasks and not cls.consumer_tasks[client_name].done():
|
|
278
|
+
logging.debug(f"消费者 '{client_name}' 已在运行中,无需重复启动")
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
if client_name not in cls.clients:
|
|
282
|
+
raise ValueError(f"RabbitMQ客户端 '{client_name}' 未初始化")
|
|
283
|
+
|
|
284
|
+
client = cls.clients[client_name]
|
|
285
|
+
handler = cls.message_handlers.get(client_name)
|
|
286
|
+
|
|
287
|
+
if not handler:
|
|
288
|
+
logging.warning(f"未找到客户端 '{client_name}' 的处理器,使用默认处理器")
|
|
289
|
+
handler = cls.default_message_handler
|
|
290
|
+
|
|
291
|
+
client.set_message_handler(handler)
|
|
292
|
+
|
|
293
|
+
stop_event = asyncio.Event()
|
|
294
|
+
cls._consumer_events[client_name] = stop_event
|
|
295
|
+
|
|
296
|
+
async def consume_task():
|
|
297
|
+
try:
|
|
298
|
+
consumer_tag = await client.start_consuming()
|
|
299
|
+
cls._consumer_tags[client_name] = consumer_tag
|
|
300
|
+
logging.info(f"消费者 '{client_name}' 开始消费,tag: {consumer_tag}")
|
|
301
|
+
|
|
302
|
+
while not stop_event.is_set():
|
|
303
|
+
await asyncio.sleep(0.1)
|
|
304
|
+
|
|
305
|
+
logging.info(f"消费者 '{client_name}' 退出循环")
|
|
306
|
+
|
|
307
|
+
except asyncio.CancelledError:
|
|
308
|
+
logging.info(f"消费者 '{client_name}' 被取消")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logging.error(
|
|
311
|
+
f"消费者 '{client_name}' 错误: {str(e)}", exc_info=True)
|
|
312
|
+
finally:
|
|
313
|
+
await client.stop_consuming()
|
|
314
|
+
logging.info(f"消费者 '{client_name}' 已完成清理")
|
|
315
|
+
|
|
316
|
+
task = asyncio.create_task(
|
|
317
|
+
consume_task(), name=f"consumer-{client_name}")
|
|
318
|
+
cls.consumer_tasks[client_name] = task
|
|
319
|
+
|
|
320
|
+
def task_exception_handler(t: asyncio.Task):
|
|
321
|
+
try:
|
|
322
|
+
if t.done():
|
|
323
|
+
t.result()
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logging.error(f"消费者任务 '{client_name}' 异常: {str(e)}")
|
|
326
|
+
|
|
327
|
+
task.add_done_callback(task_exception_handler)
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
async def default_message_handler(cls, parsed_data: MQMsgModel, original_message):
|
|
331
|
+
"""默认消息处理器"""
|
|
332
|
+
logging.info(f"\n===== 收到消息 [{original_message.routing_key}] =====")
|
|
333
|
+
logging.info(f"关联ID: {parsed_data.correlationDataId}")
|
|
334
|
+
logging.info(f"主题代码: {parsed_data.topicCode}")
|
|
335
|
+
logging.info(f"消息内容: {parsed_data.msg}")
|
|
336
|
+
logging.info("===================\n")
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def get_sender(cls, client_name: Optional[str] = None) -> Optional[RabbitMQClient]:
|
|
340
|
+
"""获取发送客户端(仅返回已注册的客户端,移除模糊匹配)"""
|
|
341
|
+
if not client_name:
|
|
342
|
+
logging.warning("发送器名称不能为空")
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
# 仅精确匹配已注册的客户端,不做模糊匹配
|
|
346
|
+
if client_name in cls.clients:
|
|
347
|
+
return cls.clients[client_name]
|
|
348
|
+
|
|
349
|
+
# 不允许通过拼接app-name反向查找(避免绕过注册)
|
|
350
|
+
app_name = cls.config.get("APP_NAME", "") if cls.config else ""
|
|
351
|
+
if app_name and not client_name.endswith(f".{app_name}"):
|
|
352
|
+
# 不尝试拼接app-name查找,确保只有注册的名称可被找到
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
logging.debug(f"发送器 '{client_name}' 不在已注册客户端列表中")
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
async def send_message(
|
|
360
|
+
cls,
|
|
361
|
+
data: Union[BaseModel, str, Dict[str, Any], None],
|
|
362
|
+
queue_name: Optional[str] = None, ** kwargs
|
|
363
|
+
) -> None:
|
|
364
|
+
"""发送消息到RabbitMQ"""
|
|
365
|
+
sender = cls.get_sender(queue_name)
|
|
366
|
+
|
|
367
|
+
if not sender:
|
|
368
|
+
error_msg = f"未找到可用的RabbitMQ发送器 (queue_name: {queue_name})"
|
|
369
|
+
logging.error(error_msg)
|
|
370
|
+
raise ValueError(error_msg)
|
|
371
|
+
|
|
372
|
+
if not sender.connection or sender.connection.is_closed:
|
|
373
|
+
logging.info(f"发送器 '{queue_name}' 连接已关闭,尝试重新连接")
|
|
374
|
+
try:
|
|
375
|
+
await sender.connect(force_reconnect=True)
|
|
376
|
+
except Exception as e:
|
|
377
|
+
logging.error(f"发送器 '{queue_name}' 重新连接失败: {str(e)}")
|
|
378
|
+
raise
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
msg = ''
|
|
382
|
+
if isinstance(data, str):
|
|
383
|
+
msg = data
|
|
384
|
+
elif isinstance(data, BaseModel):
|
|
385
|
+
msg = data.model_dump_json()
|
|
386
|
+
elif isinstance(data, dict):
|
|
387
|
+
import json
|
|
388
|
+
msg = json.dumps(data)
|
|
389
|
+
|
|
390
|
+
mq_message = MQMsgModel(
|
|
391
|
+
topicCode=queue_name.split('.')[0] if queue_name else "",
|
|
392
|
+
msg=msg,
|
|
393
|
+
correlationDataId=kwargs.get(
|
|
394
|
+
'correlationDataId', SYLogger.get_trace_id()),
|
|
395
|
+
groupId=kwargs.get('groupId', ''),
|
|
396
|
+
dataKey=kwargs.get('dataKey', ""),
|
|
397
|
+
manualFlag=kwargs.get('manualFlag', False),
|
|
398
|
+
traceId=SYLogger.get_trace_id()
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# 不设置Java会解析失败导致丢掉消息
|
|
402
|
+
mq_header = {
|
|
403
|
+
"context": SsoUser(
|
|
404
|
+
tenant_id="T000002",
|
|
405
|
+
customer_id="SYSTEM",
|
|
406
|
+
user_id="SYSTEM",
|
|
407
|
+
user_name="SYSTEM",
|
|
408
|
+
request_path="",
|
|
409
|
+
req_type="SYSTEM",
|
|
410
|
+
trace_id=SYLogger.get_trace_id(),
|
|
411
|
+
).model_dump_json()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await sender.send_message(
|
|
415
|
+
message_body=mq_message.model_dump_json(),
|
|
416
|
+
headers=mq_header,
|
|
417
|
+
)
|
|
418
|
+
logging.info(
|
|
419
|
+
f"消息发送成功 (客户端: {queue_name or cls.sender_client_names[0]})")
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logging.error(f"消息发送失败: {str(e)}", exc_info=True)
|
|
422
|
+
raise
|
|
423
|
+
|
|
424
|
+
@classmethod
|
|
425
|
+
async def shutdown(cls, timeout: float = 5.0) -> None:
|
|
426
|
+
"""优雅关闭所有客户端和消费者任务"""
|
|
427
|
+
start_time = asyncio.get_event_loop().time()
|
|
428
|
+
|
|
429
|
+
for client_name, event in cls._consumer_events.items():
|
|
430
|
+
event.set()
|
|
431
|
+
logging.info(f"已向消费者 '{client_name}' 发送退出信号")
|
|
432
|
+
|
|
433
|
+
remaining_time = max(
|
|
434
|
+
0.0, timeout - (asyncio.get_event_loop().time() - start_time))
|
|
435
|
+
if remaining_time > 0:
|
|
436
|
+
tasks_to_wait = [
|
|
437
|
+
t for t in cls.consumer_tasks.values() if not t.done()]
|
|
438
|
+
if tasks_to_wait:
|
|
439
|
+
try:
|
|
440
|
+
done, pending = await asyncio.wait(
|
|
441
|
+
tasks_to_wait,
|
|
442
|
+
timeout=remaining_time,
|
|
443
|
+
return_when=asyncio.ALL_COMPLETED
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
for task in pending:
|
|
447
|
+
task_name = task.get_name()
|
|
448
|
+
logging.warning(f"任务 '{task_name}' 关闭超时,强制取消")
|
|
449
|
+
task.cancel()
|
|
450
|
+
try:
|
|
451
|
+
await task
|
|
452
|
+
except (asyncio.CancelledError, RuntimeError):
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logging.error(f"等待消费者任务完成时出错: {str(e)}")
|
|
457
|
+
|
|
458
|
+
remaining_time = max(
|
|
459
|
+
0.0, timeout - (asyncio.get_event_loop().time() - start_time))
|
|
460
|
+
if remaining_time > 0:
|
|
461
|
+
for name, client in cls.clients.items():
|
|
462
|
+
try:
|
|
463
|
+
await asyncio.wait_for(client.stop_consuming(), timeout=remaining_time/len(cls.clients))
|
|
464
|
+
await asyncio.wait_for(client.close(), timeout=remaining_time/len(cls.clients))
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logging.warning(f"关闭客户端 '{name}' 时出错: {str(e)}")
|
|
467
|
+
logging.info(f"RabbitMQ客户端 '{name}' 已关闭")
|
|
468
|
+
|
|
469
|
+
cls.consumer_tasks.clear()
|
|
470
|
+
cls._consumer_events.clear()
|
|
471
|
+
cls._consumer_tags.clear()
|
|
472
|
+
cls.clients.clear()
|
|
473
|
+
cls.sender_client_names.clear()
|
|
474
|
+
cls._init_lock.clear()
|
|
475
|
+
|
|
476
|
+
logging.info("RabbitMQ服务已完全关闭")
|
sycommon/services.py
CHANGED
|
@@ -1,29 +1,173 @@
|
|
|
1
|
-
from typing import Callable, List, Tuple
|
|
2
|
-
|
|
1
|
+
from typing import Any, Callable, Dict, List, Tuple, Union, Optional, Awaitable
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from pydantic import BaseModel
|
|
3
6
|
from sycommon.config.Config import SingletonMeta
|
|
7
|
+
from sycommon.models.mqlistener_config import RabbitMQListenerConfig
|
|
8
|
+
from sycommon.models.mqsend_config import RabbitMQSendConfig
|
|
9
|
+
from sycommon.rabbitmq.rabbitmq_service import RabbitMQService
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
class Services(metaclass=SingletonMeta):
|
|
13
|
+
_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
14
|
+
_config: Optional[dict] = None
|
|
15
|
+
_initialized: bool = False
|
|
16
|
+
_registered_senders: List[str] = []
|
|
17
|
+
_mq_tasks: List[asyncio.Task] = []
|
|
18
|
+
_pending_setup: Optional[Callable[..., Awaitable[None]]] = None
|
|
19
|
+
|
|
7
20
|
def __init__(self, config):
|
|
8
|
-
|
|
21
|
+
if not Services._config:
|
|
22
|
+
Services._config = config
|
|
23
|
+
RabbitMQService.init(config)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get_lifespan(cls, config):
|
|
27
|
+
"""返回 FastAPI 的 lifespan 管理器"""
|
|
28
|
+
@asynccontextmanager
|
|
29
|
+
async def lifespan(app):
|
|
30
|
+
# 应用启动时初始化
|
|
31
|
+
cls._config = config
|
|
32
|
+
cls._loop = asyncio.get_running_loop()
|
|
33
|
+
cls._initialized = True
|
|
34
|
+
logging.info("Services initialized with FastAPI event loop")
|
|
35
|
+
|
|
36
|
+
# 执行之前缓存的MQ设置
|
|
37
|
+
if cls._pending_setup:
|
|
38
|
+
try:
|
|
39
|
+
await cls._pending_setup()
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logging.error(f"执行MQ初始化失败: {str(e)}", exc_info=True)
|
|
42
|
+
finally:
|
|
43
|
+
cls._pending_setup = None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
yield
|
|
47
|
+
finally:
|
|
48
|
+
# 应用关闭时清理
|
|
49
|
+
await cls.shutdown()
|
|
50
|
+
logging.info("Services shutdown completed")
|
|
9
51
|
|
|
10
|
-
|
|
11
|
-
|
|
52
|
+
return lifespan
|
|
53
|
+
|
|
54
|
+
def plugins(
|
|
55
|
+
self,
|
|
56
|
+
nacos_service: Optional[Callable] = None,
|
|
57
|
+
logging_service: Optional[Callable] = None,
|
|
58
|
+
database_service: Optional[Union[Tuple[Callable,
|
|
59
|
+
str], List[Tuple[Callable, str]]]] = None,
|
|
60
|
+
rabbitmq_listeners: Optional[List[RabbitMQListenerConfig]] = None,
|
|
61
|
+
rabbitmq_senders: Optional[List[RabbitMQSendConfig]] = None
|
|
62
|
+
) -> None:
|
|
63
|
+
"""注册各种服务插件"""
|
|
64
|
+
# 注册非异步服务
|
|
12
65
|
if nacos_service:
|
|
13
|
-
nacos_service(self.
|
|
14
|
-
# 注册日志服务
|
|
66
|
+
nacos_service(self._config)
|
|
15
67
|
if logging_service:
|
|
16
|
-
logging_service(self.
|
|
17
|
-
# 注册数据库服务
|
|
68
|
+
logging_service(self._config)
|
|
18
69
|
if database_service:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
70
|
+
self._setup_database(database_service)
|
|
71
|
+
|
|
72
|
+
# MQ设置异步函数
|
|
73
|
+
async def setup_mq_components():
|
|
74
|
+
if rabbitmq_senders:
|
|
75
|
+
self._setup_senders(rabbitmq_senders)
|
|
76
|
+
if rabbitmq_listeners:
|
|
77
|
+
self._setup_listeners(rabbitmq_listeners)
|
|
78
|
+
|
|
79
|
+
# 存储到_pending_setup,等待lifespan异步执行
|
|
80
|
+
Services._pending_setup = setup_mq_components
|
|
81
|
+
|
|
82
|
+
def _setup_database(self, database_service):
|
|
83
|
+
if isinstance(database_service, tuple):
|
|
84
|
+
db_setup, db_name = database_service
|
|
85
|
+
db_setup(self._config, db_name)
|
|
86
|
+
elif isinstance(database_service, list):
|
|
87
|
+
for db_setup, db_name in database_service:
|
|
88
|
+
db_setup(self._config, db_name)
|
|
89
|
+
|
|
90
|
+
def _setup_senders(self, rabbitmq_senders):
|
|
91
|
+
Services._registered_senders = [
|
|
92
|
+
sender.queue_name for sender in rabbitmq_senders]
|
|
93
|
+
if self._loop: # 确保loop存在
|
|
94
|
+
task = self._loop.create_task(
|
|
95
|
+
self._setup_and_wait(
|
|
96
|
+
RabbitMQService.setup_senders, rabbitmq_senders)
|
|
97
|
+
)
|
|
98
|
+
self._mq_tasks.append(task)
|
|
99
|
+
logging.info(f"已注册的RabbitMQ发送器: {Services._registered_senders}")
|
|
100
|
+
|
|
101
|
+
def _setup_listeners(self, rabbitmq_listeners):
|
|
102
|
+
if self._loop: # 确保loop存在
|
|
103
|
+
task = self._loop.create_task(
|
|
104
|
+
self._setup_and_wait(
|
|
105
|
+
RabbitMQService.setup_listeners, rabbitmq_listeners)
|
|
106
|
+
)
|
|
107
|
+
self._mq_tasks.append(task)
|
|
108
|
+
|
|
109
|
+
async def _setup_and_wait(self, setup_func, *args, **kwargs):
|
|
110
|
+
try:
|
|
111
|
+
await setup_func(*args, **kwargs)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logging.error(
|
|
114
|
+
f"Error in {setup_func.__name__}: {str(e)}", exc_info=True)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
async def send_message(
|
|
118
|
+
cls,
|
|
119
|
+
queue_name: str,
|
|
120
|
+
data: Union[str, Dict[str, Any], BaseModel, None],
|
|
121
|
+
max_retries: int = 3, # 最大重试次数
|
|
122
|
+
retry_delay: float = 1.0, # 重试间隔(秒)
|
|
123
|
+
**kwargs
|
|
124
|
+
) -> None:
|
|
125
|
+
"""发送消息,添加重试机制处理发送器不存在的情况"""
|
|
126
|
+
if not cls._initialized or not cls._loop:
|
|
127
|
+
logging.error("Services not properly initialized!")
|
|
128
|
+
raise ValueError("服务未正确初始化")
|
|
129
|
+
|
|
130
|
+
# 重试逻辑
|
|
131
|
+
for attempt in range(max_retries):
|
|
132
|
+
try:
|
|
133
|
+
# 检查发送器是否已注册
|
|
134
|
+
if queue_name not in cls._registered_senders:
|
|
135
|
+
# 可能是初始化尚未完成,尝试刷新注册列表
|
|
136
|
+
cls._registered_senders = RabbitMQService.sender_client_names
|
|
137
|
+
if queue_name not in cls._registered_senders:
|
|
138
|
+
raise ValueError(f"发送器 {queue_name} 未注册")
|
|
139
|
+
|
|
140
|
+
# 获取发送器
|
|
141
|
+
sender = RabbitMQService.get_sender(queue_name)
|
|
142
|
+
if not sender:
|
|
143
|
+
raise ValueError(f"发送器 '{queue_name}' 不存在")
|
|
144
|
+
|
|
145
|
+
# 发送消息
|
|
146
|
+
await RabbitMQService.send_message(data, queue_name, ** kwargs)
|
|
147
|
+
logging.info(f"消息发送成功(尝试 {attempt+1}/{max_retries})")
|
|
148
|
+
return # 成功发送,退出函数
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
# 最后一次尝试失败则抛出异常
|
|
152
|
+
if attempt == max_retries - 1:
|
|
153
|
+
logging.error(
|
|
154
|
+
f"消息发送失败(已尝试 {max_retries} 次): {str(e)}", exc_info=True)
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
# 非最后一次尝试,记录警告并等待重试
|
|
158
|
+
logging.warning(
|
|
159
|
+
f"消息发送失败(尝试 {attempt+1}/{max_retries}): {str(e)},"
|
|
160
|
+
f"{retry_delay}秒后重试..."
|
|
161
|
+
)
|
|
162
|
+
await asyncio.sleep(retry_delay)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
async def shutdown():
|
|
166
|
+
for task in Services._mq_tasks:
|
|
167
|
+
if not task.done():
|
|
168
|
+
task.cancel()
|
|
169
|
+
try:
|
|
170
|
+
await task
|
|
171
|
+
except asyncio.CancelledError:
|
|
172
|
+
pass
|
|
173
|
+
await RabbitMQService.shutdown()
|