hello-datap-component-base 0.2.0__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.
@@ -0,0 +1,286 @@
1
+ """
2
+ 阿里云 MNS 消息队列客户端
3
+ """
4
+ import json
5
+ import os
6
+ import time
7
+ from typing import Dict, Any, Optional
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MNSClient:
14
+ """阿里云 MNS 消息队列客户端"""
15
+
16
+ def __init__(
17
+ self,
18
+ queue_name: str = "aiinfra-data-process-component-result-queue",
19
+ max_retries: int = 3,
20
+ retry_delay: float = 1.0,
21
+ retry_backoff: float = 2.0
22
+ ):
23
+ """
24
+ 初始化 MNS 客户端
25
+
26
+ Args:
27
+ queue_name: 队列名称,默认为 aiinfra-data-process-component-result-queue
28
+ max_retries: 最大重试次数,默认 3 次(可通过环境变量 MNS_MAX_RETRIES 配置)
29
+ retry_delay: 初始重试延迟(秒),默认 1.0 秒(可通过环境变量 MNS_RETRY_DELAY 配置)
30
+ retry_backoff: 重试延迟的指数退避倍数,默认 2.0(可通过环境变量 MNS_RETRY_BACKOFF 配置)
31
+ """
32
+ self.queue_name = queue_name
33
+
34
+ # 从环境变量读取重试配置,如果没有则使用默认值
35
+ self.max_retries = int(os.environ.get('MNS_MAX_RETRIES', max_retries))
36
+ self.retry_delay = float(os.environ.get('MNS_RETRY_DELAY', retry_delay))
37
+ self.retry_backoff = float(os.environ.get('MNS_RETRY_BACKOFF', retry_backoff))
38
+
39
+ self._client = None
40
+ self._queue = None
41
+ self._initialized = False
42
+ self._Message = None # Message 类,延迟加载
43
+
44
+ def _init_client(self):
45
+ """初始化 MNS 客户端(延迟加载)"""
46
+ if self._initialized:
47
+ return
48
+
49
+ try:
50
+ # 尝试导入阿里云 MNS SDK
51
+ from mns.account import Account
52
+ from mns.queue import Queue, Message
53
+
54
+ # 保存 Message 类供后续使用
55
+ self._Message = Message
56
+
57
+ # 从环境变量获取 MNS 配置
58
+ endpoint = os.environ.get('MNS_ENDPOINT')
59
+ access_key_id = os.environ.get('MNS_ACCESS_KEY_ID')
60
+ access_key_secret = os.environ.get('MNS_ACCESS_KEY_SECRET')
61
+
62
+ if not all([endpoint, access_key_id, access_key_secret]):
63
+ # 如果 MNS_ENDPOINT 不存在,静默跳过(已在 get_mns_client 中检查)
64
+ # 如果 MNS_ENDPOINT 存在但其他配置不完整,记录警告
65
+ if endpoint:
66
+ logger.warning(
67
+ "MNS 配置不完整,无法发送消息到队列。"
68
+ "需要设置环境变量: MNS_ACCESS_KEY_ID, MNS_ACCESS_KEY_SECRET"
69
+ )
70
+ self._initialized = True # 标记为已初始化,避免重复警告
71
+ return
72
+
73
+ # 创建 MNS 账户
74
+ account = Account(endpoint, access_key_id, access_key_secret)
75
+ # 获取队列(使用 get_queue 方法)
76
+ self._queue = account.get_queue(self.queue_name)
77
+ self._client = account
78
+ self._initialized = True
79
+ logger.info(f"MNS 客户端初始化成功,队列: {self.queue_name}")
80
+
81
+ except ImportError:
82
+ logger.warning(
83
+ "未安装阿里云 MNS SDK,无法发送消息到队列。"
84
+ "请安装: pip install aliyun-mns"
85
+ )
86
+ self._initialized = True # 标记为已初始化,避免重复警告
87
+ except Exception as e:
88
+ logger.error(f"初始化 MNS 客户端失败: {e}")
89
+ self._initialized = True # 标记为已初始化,避免重复警告
90
+
91
+ def _is_retryable_exception(self, exception: Exception) -> bool:
92
+ """
93
+ 判断异常是否可重试
94
+
95
+ Args:
96
+ exception: 异常对象
97
+
98
+ Returns:
99
+ 是否可重试
100
+ """
101
+ exception_str = str(exception)
102
+ exception_type = type(exception).__name__
103
+
104
+ # 网络相关异常,可重试
105
+ retryable_keywords = [
106
+ 'NetworkException',
107
+ 'NetWorkException',
108
+ 'Connection reset',
109
+ 'Connection refused',
110
+ 'Connection timeout',
111
+ 'timeout',
112
+ 'ConnectionError',
113
+ 'ConnectTimeout',
114
+ 'ReadTimeout',
115
+ 'Errno 104', # Connection reset by peer
116
+ 'Errno 111', # Connection refused
117
+ 'Errno 110', # Connection timed out
118
+ ]
119
+
120
+ # 检查异常类型和消息中是否包含可重试的关键词
121
+ for keyword in retryable_keywords:
122
+ if keyword in exception_type or keyword in exception_str:
123
+ return True
124
+
125
+ return False
126
+
127
+ def _send_message_once(self, message_body: str) -> bool:
128
+ """
129
+ 发送消息的单次尝试
130
+
131
+ Args:
132
+ message_body: JSON 格式的消息字符串
133
+
134
+ Returns:
135
+ 是否发送成功
136
+ """
137
+ # 创建 Message 对象(MNS SDK 要求使用 Message 对象)
138
+ if self._Message is None:
139
+ from mns.queue import Message
140
+ self._Message = Message
141
+
142
+ msg = self._Message(message_body)
143
+
144
+ # 发送消息
145
+ self._queue.send_message(msg)
146
+ return True
147
+
148
+ def send_message(self, message: Dict[str, Any]) -> bool:
149
+ """
150
+ 发送消息到队列(带重试逻辑)
151
+
152
+ Args:
153
+ message: 要发送的消息字典
154
+
155
+ Returns:
156
+ 是否发送成功
157
+ """
158
+ self._init_client()
159
+
160
+ if not self._queue:
161
+ logger.warning("MNS 队列未初始化,跳过消息发送")
162
+ return False
163
+
164
+ # 将消息转换为 JSON 字符串
165
+ message_body = json.dumps(message, ensure_ascii=False)
166
+
167
+ # 重试逻辑
168
+ last_exception = None
169
+ delay = self.retry_delay
170
+
171
+ for attempt in range(self.max_retries + 1): # 总共尝试 max_retries + 1 次
172
+ try:
173
+ success = self._send_message_once(message_body)
174
+ if success:
175
+ if attempt > 0:
176
+ logger.info(
177
+ f"消息已成功发送到队列 {self.queue_name} "
178
+ f"(第 {attempt + 1} 次尝试)"
179
+ )
180
+ else:
181
+ logger.info(f"消息已发送到队列 {self.queue_name}")
182
+ return True
183
+
184
+ except Exception as e:
185
+ last_exception = e
186
+
187
+ # 判断是否可重试
188
+ if not self._is_retryable_exception(e):
189
+ # 不可重试的异常,直接返回失败
190
+ logger.error(
191
+ f"发送消息到队列失败(不可重试的异常): {e}",
192
+ exc_info=True
193
+ )
194
+ return False
195
+
196
+ # 可重试的异常
197
+ if attempt < self.max_retries:
198
+ # 还有重试机会
199
+ logger.warning(
200
+ f"发送消息到队列失败(第 {attempt + 1} 次尝试): {e},"
201
+ f"{delay:.2f} 秒后重试..."
202
+ )
203
+ time.sleep(delay)
204
+ delay *= self.retry_backoff # 指数退避
205
+ else:
206
+ # 已用完所有重试机会
207
+ logger.error(
208
+ f"发送消息到队列失败(已重试 {self.max_retries} 次): {e}",
209
+ exc_info=True
210
+ )
211
+
212
+ # 所有重试都失败了
213
+ if last_exception:
214
+ logger.error(
215
+ f"发送消息到队列最终失败(共尝试 {self.max_retries + 1} 次): {last_exception}",
216
+ exc_info=True
217
+ )
218
+ return False
219
+
220
+ def send_result(
221
+ self,
222
+ code: int,
223
+ message: str,
224
+ work_flow_id: Optional[int],
225
+ work_flow_instance_id: Optional[int],
226
+ task_id: Optional[str],
227
+ output: Optional[Dict[str, Any]] = None,
228
+ processing_time: Optional[float] = None
229
+ ) -> bool:
230
+ """
231
+ 发送处理结果到队列
232
+
233
+ Args:
234
+ code: 返回码,0 表示成功,非 0 表示失败
235
+ message: 返回消息
236
+ work_flow_id: 工作流ID
237
+ work_flow_instance_id: 工作流实例ID
238
+ task_id: 任务ID
239
+ output: 用户程序的输出结果(正常时为结果,异常时为 None)
240
+ processing_time: 处理时间(秒)
241
+
242
+ Returns:
243
+ 是否发送成功
244
+ """
245
+ result_message = {
246
+ "code": code,
247
+ "message": message,
248
+ "data": {
249
+ "work_flow_id": work_flow_id,
250
+ "work_flow_instance_id": work_flow_instance_id,
251
+ "task_id": task_id,
252
+ "out_put": output
253
+ }
254
+ }
255
+ if processing_time is not None:
256
+ result_message["processing_time"] = processing_time
257
+
258
+ return self.send_message(result_message)
259
+
260
+
261
+ def get_mns_client(queue_name: Optional[str] = None) -> Optional[MNSClient]:
262
+ """
263
+ 获取 MNS 客户端实例
264
+
265
+ Args:
266
+ queue_name: 队列名称,如果为 None 则使用默认队列名
267
+
268
+ Returns:
269
+ MNSClient 实例,如果配置不完整则返回 None
270
+ """
271
+ # 如果环境变量中没有 MNS_ENDPOINT 配置,则不发送 MNS,直接返回 None
272
+ if not os.environ.get('MNS_ENDPOINT'):
273
+ return None
274
+
275
+ if queue_name is None:
276
+ queue_name = os.environ.get('MNS_QUEUE_NAME', 'aiinfra-data-process-component-result-queue')
277
+
278
+ client = MNSClient(queue_name)
279
+ client._init_client()
280
+
281
+ # 如果客户端未正确初始化,返回 None
282
+ if not client._queue:
283
+ return None
284
+
285
+ return client
286
+
@@ -0,0 +1,247 @@
1
+ import asyncio
2
+ import signal
3
+ import sys
4
+ import subprocess
5
+ from typing import Optional, Dict, Any, Type
6
+ from .base import BaseService, ServiceConfig
7
+ from .config import ServerConfig
8
+ from .discover import get_single_service_class
9
+ from .logger import setup_logging
10
+
11
+
12
+ class ServiceRunner:
13
+ """服务运行器"""
14
+
15
+ def __init__(self, config_path: str, class_name: Optional[str] = None):
16
+ """
17
+ 初始化运行器
18
+
19
+ Args:
20
+ config_path: 配置文件路径
21
+ class_name: 指定的服务类名
22
+ """
23
+ self.config_path = config_path
24
+ self.class_name = class_name
25
+ self.config: Optional[ServerConfig] = None
26
+ self.service_class: Optional[Type[BaseService]] = None
27
+ self.service_instance: Optional[BaseService] = None
28
+
29
+ def load_config(self):
30
+ """加载配置"""
31
+ self.config = ServerConfig.from_file(self.config_path)
32
+
33
+ def discover_service(self):
34
+ """发现服务类"""
35
+ import os
36
+ # 使用当前工作目录作为搜索路径
37
+ search_path = os.getcwd()
38
+ self.service_class = get_single_service_class(
39
+ search_path=search_path,
40
+ class_name=self.class_name
41
+ )
42
+
43
+ def setup_environment(self):
44
+ """设置运行环境"""
45
+ # 设置日志
46
+ setup_logging(
47
+ level=self.config.runtime_env.env_vars.get("LOG_LEVEL", "INFO")
48
+ if self.config.runtime_env and self.config.runtime_env.env_vars
49
+ else "INFO",
50
+ json_format=False,
51
+ service_name=self.config.name,
52
+ version=self.config.version
53
+ )
54
+
55
+ # 安装pip包(如果配置了)
56
+ self._install_pip_packages()
57
+
58
+ def _install_pip_packages(self):
59
+ """安装runtime_env中指定的pip包"""
60
+ if not self.config.runtime_env or not self.config.runtime_env.pip:
61
+ return
62
+
63
+ pip_packages = self.config.runtime_env.pip
64
+ if not isinstance(pip_packages, list) or len(pip_packages) == 0:
65
+ return
66
+
67
+ print(f"Installing pip packages: {pip_packages}")
68
+
69
+ try:
70
+ # 使用 subprocess 调用 pip install
71
+ # 使用 -q 参数减少输出,--disable-pip-version-check 禁用版本检查警告
72
+ cmd = [
73
+ sys.executable, "-m", "pip", "install",
74
+ "-q", "--disable-pip-version-check"
75
+ ] + pip_packages
76
+
77
+ result = subprocess.run(
78
+ cmd,
79
+ check=True,
80
+ capture_output=True,
81
+ text=True
82
+ )
83
+
84
+ print(f"✅ Successfully installed packages: {', '.join(pip_packages)}")
85
+
86
+ except subprocess.CalledProcessError as e:
87
+ error_msg = f"Failed to install pip packages: {pip_packages}\n"
88
+ if e.stdout:
89
+ error_msg += f"stdout: {e.stdout}\n"
90
+ if e.stderr:
91
+ error_msg += f"stderr: {e.stderr}\n"
92
+ print(f"❌ {error_msg}", file=sys.stderr)
93
+ raise RuntimeError(f"Failed to install required pip packages: {error_msg}")
94
+ except Exception as e:
95
+ print(f"❌ Error installing pip packages: {e}", file=sys.stderr)
96
+ raise RuntimeError(f"Failed to install required pip packages: {e}")
97
+
98
+ def create_service_instance(self):
99
+ """创建服务实例"""
100
+ # 创建 ServiceConfig
101
+ runtime_env_dict = None
102
+ if self.config.runtime_env:
103
+ runtime_env_dict = self.config.runtime_env.model_dump(exclude_none=True)
104
+ # 如果转换后的字典为空,设置为None
105
+ if not runtime_env_dict:
106
+ runtime_env_dict = None
107
+
108
+ service_config = ServiceConfig(
109
+ name=self.config.name,
110
+ version=self.config.version,
111
+ params=self.config.params,
112
+ runtime_env=runtime_env_dict,
113
+ work_flow_id=self.config.work_flow_id,
114
+ work_flow_instance_id=self.config.work_flow_instance_id,
115
+ task_id=self.config.task_id
116
+ )
117
+
118
+ # 创建服务实例
119
+ self.service_instance = self.service_class(service_config)
120
+
121
+ async def run_async(self):
122
+ """异步运行服务并执行一次处理"""
123
+ print(f"Starting service: {self.config.name}")
124
+ if self.config.version:
125
+ print(f"Version: {self.config.version}")
126
+ print(f"Config: {self.config.model_dump_json(indent=2)}")
127
+ print("\n" + "=" * 60)
128
+ print("Processing request...")
129
+ print("=" * 60 + "\n")
130
+
131
+ try:
132
+ # 准备输入数据
133
+ # 从配置文件的 params 中获取,如果不存在则使用默认值
134
+ params = self.config.params
135
+ if params is None:
136
+ params = {}
137
+
138
+ # 执行一次处理(handle_request 会封装结果,包括异常情况)
139
+ result = await self.service_instance.handle_request(params)
140
+
141
+ # 输出结果
142
+ import json
143
+ print("=" * 60)
144
+ print("Processing Result:")
145
+ print("=" * 60)
146
+ print(json.dumps(result, indent=2, ensure_ascii=False))
147
+ print("=" * 60)
148
+
149
+ # 发送结果到 MNS 队列
150
+ self._send_result_to_mns(result)
151
+
152
+ print("Service completed successfully.")
153
+ print("=" * 60)
154
+
155
+ except Exception as e:
156
+ print(f"\nError processing request: {e}", file=sys.stderr)
157
+ import traceback
158
+ traceback.print_exc()
159
+
160
+ # 即使发生异常,也要尝试发送错误结果到队列
161
+ try:
162
+ error_result = {
163
+ "code": -1,
164
+ "message": str(e),
165
+ "data": {
166
+ "work_flow_id": self.config.work_flow_id,
167
+ "work_flow_instance_id": self.config.work_flow_instance_id,
168
+ "task_id": self.config.task_id,
169
+ "out_put": None
170
+ }
171
+ }
172
+ self._send_result_to_mns(error_result)
173
+ except Exception as mns_error:
174
+ print(f"Failed to send error result to MNS: {mns_error}", file=sys.stderr)
175
+
176
+ raise
177
+
178
+ def run(self):
179
+ """运行服务(主入口)"""
180
+ try:
181
+ # 加载配置
182
+ self.load_config()
183
+
184
+ # 发现服务类
185
+ self.discover_service()
186
+
187
+ # 设置环境
188
+ self.setup_environment()
189
+
190
+ # 创建服务实例
191
+ self.create_service_instance()
192
+
193
+ # 运行服务
194
+ asyncio.run(self.run_async())
195
+
196
+ except KeyboardInterrupt:
197
+ print("\nService stopped by user")
198
+ except Exception as e:
199
+ print(f"Error running service: {e}", file=sys.stderr)
200
+ sys.exit(1)
201
+
202
+ def _send_result_to_mns(self, result: Dict[str, Any]):
203
+ """
204
+ 发送结果到 MNS 队列
205
+
206
+ Args:
207
+ result: 封装后的结果字典
208
+ """
209
+ try:
210
+ from .mns_client import get_mns_client
211
+ import os
212
+
213
+ # 如果环境变量中没有 MNS_ENDPOINT 配置,则不发送 MNS,静默跳过
214
+ if not os.environ.get('MNS_ENDPOINT'):
215
+ return
216
+
217
+ client = get_mns_client()
218
+ if client:
219
+ # result 已经是封装好的格式,直接发送
220
+ success = client.send_message(result)
221
+ if success:
222
+ print("✅ Result sent to MNS queue successfully")
223
+ else:
224
+ print("⚠️ Failed to send result to MNS queue")
225
+ # 如果 client 为 None(配置不完整),静默跳过,不打印警告
226
+ except Exception as e:
227
+ print(f"⚠️ Error sending result to MNS queue: {e}", file=sys.stderr)
228
+
229
+ async def process_request(self, data: Dict[str, Any]) -> Dict[str, Any]:
230
+ """
231
+ 处理请求(供外部调用)
232
+
233
+ Args:
234
+ data: 输入数据
235
+
236
+ Returns:
237
+ 处理结果
238
+ """
239
+ if not self.service_instance:
240
+ raise RuntimeError("Service not started")
241
+
242
+ result = await self.service_instance.handle_request(data)
243
+
244
+ # 发送结果到 MNS 队列
245
+ self._send_result_to_mns(result)
246
+
247
+ return result