adp-operator 1.0.7__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.
- adp_operator-1.0.7/PKG-INFO +81 -0
- adp_operator-1.0.7/README.md +57 -0
- adp_operator-1.0.7/adp_operator/__init__.py +0 -0
- adp_operator-1.0.7/adp_operator/c2c/__init__.py +6 -0
- adp_operator-1.0.7/adp_operator/c2c/__main__.py +5 -0
- adp_operator-1.0.7/adp_operator/c2c/client.py +128 -0
- adp_operator-1.0.7/adp_operator/c2c/commands.py +181 -0
- adp_operator-1.0.7/adp_operator/c2c/config.py +11 -0
- adp_operator-1.0.7/adp_operator/c2c/discovery.py +314 -0
- adp_operator-1.0.7/adp_operator/c2c/event_bus.py +129 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/__init__.py +2 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/data.py +11 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/discovery.py +126 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/file.py +179 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/identity.py +25 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/operator.py +90 -0
- adp_operator-1.0.7/adp_operator/c2c/handlers/scan.py +58 -0
- adp_operator-1.0.7/adp_operator/c2c/log_stream.py +68 -0
- adp_operator-1.0.7/adp_operator/c2c/main.py +46 -0
- adp_operator-1.0.7/adp_operator/c2c/node_context.py +116 -0
- adp_operator-1.0.7/adp_operator/c2c/operator.py +70 -0
- adp_operator-1.0.7/adp_operator/c2c/operator_executor.py +468 -0
- adp_operator-1.0.7/adp_operator/c2c/operator_loader.py +25 -0
- adp_operator-1.0.7/adp_operator/c2c/operator_metadata.py +34 -0
- adp_operator-1.0.7/adp_operator/c2c/operator_service.py +52 -0
- adp_operator-1.0.7/adp_operator/c2c/operator_source.py +57 -0
- adp_operator-1.0.7/adp_operator/c2c/peer_registry.py +388 -0
- adp_operator-1.0.7/adp_operator/c2c/protocol.py +210 -0
- adp_operator-1.0.7/adp_operator/c2c/reverse_connection.py +538 -0
- adp_operator-1.0.7/adp_operator/c2c/routing.py +180 -0
- adp_operator-1.0.7/adp_operator/c2c/run_queue.py +40 -0
- adp_operator-1.0.7/adp_operator/c2c/runtime.py +296 -0
- adp_operator-1.0.7/adp_operator/c2c/service_discovery.py +58 -0
- adp_operator-1.0.7/adp_operator/c2c/session.py +100 -0
- adp_operator-1.0.7/adp_operator/c2c/socket_channel.py +322 -0
- adp_operator-1.0.7/adp_operator/c2c/state.py +58 -0
- adp_operator-1.0.7/adp_operator/c2c/tcp_server.py +411 -0
- adp_operator-1.0.7/adp_operator/c2c/websocket_server.py +240 -0
- adp_operator-1.0.7/adp_operator/cache_utils.py +35 -0
- adp_operator-1.0.7/adp_operator/connect.py +91 -0
- adp_operator-1.0.7/adp_operator/core/__init__.py +12 -0
- adp_operator-1.0.7/adp_operator/core/constants.py +15 -0
- adp_operator-1.0.7/adp_operator/core/json2pdf.py +271 -0
- adp_operator-1.0.7/adp_operator/core/llm_config.py +39 -0
- adp_operator-1.0.7/adp_operator/core/models.py +150 -0
- adp_operator-1.0.7/adp_operator/core/my_scale_db_storeage.py +35 -0
- adp_operator-1.0.7/adp_operator/core/object_storage/__init__.py +0 -0
- adp_operator-1.0.7/adp_operator/core/object_storage/base.py +45 -0
- adp_operator-1.0.7/adp_operator/core/object_storage/base_stream_object.py +3 -0
- adp_operator-1.0.7/adp_operator/core/object_storage/minio_object_storage.py +213 -0
- adp_operator-1.0.7/adp_operator/core/object_storage/tos_object_storage.py +225 -0
- adp_operator-1.0.7/adp_operator/core/operator_init_config.py +121 -0
- adp_operator-1.0.7/adp_operator/core/operator_utils.py +80 -0
- adp_operator-1.0.7/adp_operator/core/run_and_load_node_info.py +115 -0
- adp_operator-1.0.7/adp_operator/dataset/__init__.py +5 -0
- adp_operator-1.0.7/adp_operator/dataset/cache.py +189 -0
- adp_operator-1.0.7/adp_operator/dataset/storage.py +25 -0
- adp_operator-1.0.7/adp_operator/logger.py +113 -0
- adp_operator-1.0.7/adp_operator/management/__init__.py +1 -0
- adp_operator-1.0.7/adp_operator/management/commands/__init__.py +1 -0
- adp_operator-1.0.7/adp_operator/management/commands/common.py +43 -0
- adp_operator-1.0.7/adp_operator/management/commands/login.py +54 -0
- adp_operator-1.0.7/adp_operator/management/commands/logs.py +19 -0
- adp_operator-1.0.7/adp_operator/management/commands/run.py +56 -0
- adp_operator-1.0.7/adp_operator/management/commands/upload.py +109 -0
- adp_operator-1.0.7/adp_operator/management/main.py +78 -0
- adp_operator-1.0.7/adp_operator/myscale_pool.py +163 -0
- adp_operator-1.0.7/adp_operator/myscale_utils.py +90 -0
- adp_operator-1.0.7/adp_operator/storage.py +1184 -0
- adp_operator-1.0.7/adp_operator/utils/__init__.py +0 -0
- adp_operator-1.0.7/adp_operator/utils/dataframe_transport.py +42 -0
- adp_operator-1.0.7/adp_operator/utils/docker_api.py +46 -0
- adp_operator-1.0.7/adp_operator/utils/file_utils.py +57 -0
- adp_operator-1.0.7/adp_operator/utils/myscale_pool.py +187 -0
- adp_operator-1.0.7/adp_operator/utils/myscale_utils.py +389 -0
- adp_operator-1.0.7/adp_operator.egg-info/PKG-INFO +81 -0
- adp_operator-1.0.7/adp_operator.egg-info/SOURCES.txt +81 -0
- adp_operator-1.0.7/adp_operator.egg-info/dependency_links.txt +1 -0
- adp_operator-1.0.7/adp_operator.egg-info/entry_points.txt +2 -0
- adp_operator-1.0.7/adp_operator.egg-info/requires.txt +12 -0
- adp_operator-1.0.7/adp_operator.egg-info/top_level.txt +1 -0
- adp_operator-1.0.7/setup.cfg +4 -0
- adp_operator-1.0.7/setup.py +41 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adp-operator
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: ADP operator management and C2C runtime tools
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: docker
|
|
8
|
+
Requires-Dist: fastapi
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: pyarrow
|
|
11
|
+
Requires-Dist: pyarmor
|
|
12
|
+
Requires-Dist: psutil
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: python-multipart
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: uvicorn
|
|
17
|
+
Requires-Dist: colorlog
|
|
18
|
+
Requires-Dist: websockets
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: requires-dist
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
Dynamic: summary
|
|
24
|
+
|
|
25
|
+
# ADP Operator
|
|
26
|
+
|
|
27
|
+
## C2C 连接首包
|
|
28
|
+
|
|
29
|
+
C2C TCP 连接建立后,双方必须先交换固定长度的二进制首包。首包校验通过后,连接才允许继续处理 JSON 业务指令。
|
|
30
|
+
|
|
31
|
+
首包固定为 512 bit,结构如下:
|
|
32
|
+
|
|
33
|
+
| bit 偏移 | bit 长度 | 字段 | 规则 |
|
|
34
|
+
| ---: | ---: | --- | --- |
|
|
35
|
+
| 0 | 24 | 固定标识 | 三个 ASCII 字符 `ADP` |
|
|
36
|
+
| 24 | 16 | 协议版本 | 16-bit 无符号整数,使用大端序,当前版本为 `7` |
|
|
37
|
+
| 40 | 256 | 实例编号 | 当前节点的 `instance_id`,固定32字节 |
|
|
38
|
+
| 296 | 8 | 功能控制区 | bit 0–2为功能开关,bit 3–7为保留位 |
|
|
39
|
+
| 304 | 208 | 全零保留区 | 必须全部为 `0` |
|
|
40
|
+
|
|
41
|
+
字符串字段统一使用 UTF-8 编码。字段长度不足时在尾部使用 `0x00` 填充,超过字段长度时按字段长度截断。
|
|
42
|
+
|
|
43
|
+
### 功能控制区
|
|
44
|
+
|
|
45
|
+
功能控制区固定为8 bit,每个功能占用一个 bit:
|
|
46
|
+
|
|
47
|
+
| bit | 功能 | `0` | `1` | 当前默认值 |
|
|
48
|
+
| ---: | --- | --- | --- | --- |
|
|
49
|
+
| 0 | UDP 主动发现 | 关闭 | 开启 | `1` |
|
|
50
|
+
| 1 | 额外 channel 通道 | 不支持 | 支持 | `1` |
|
|
51
|
+
| 2 | WebSocket 业务端口 | 关闭 | 开启 | `1` |
|
|
52
|
+
| 3–7 | 保留位 | 必须为零 | 不允许 | `0` |
|
|
53
|
+
|
|
54
|
+
当前默认控制区的二进制内容为:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
00000111
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 握手流程
|
|
61
|
+
|
|
62
|
+
1. TCP 客户端连接 C2C 服务端。
|
|
63
|
+
2. 客户端发送自己的 512 bit 二进制首包。
|
|
64
|
+
3. 服务端读取并校验完整首包。
|
|
65
|
+
4. 服务端校验通过后,向客户端返回自己的 512 bit 二进制首包。
|
|
66
|
+
5. 客户端校验服务端首包。
|
|
67
|
+
6. 双方校验均通过后,才开始发送 JSON 业务指令。
|
|
68
|
+
|
|
69
|
+
服务端会拒绝以下连接:
|
|
70
|
+
|
|
71
|
+
- 首包长度不足 512 bit;
|
|
72
|
+
- 固定标识不是 `ADP`;
|
|
73
|
+
- 目标节点协议版本低于发起连接节点的协议版本;
|
|
74
|
+
- `instance_id` 为空;
|
|
75
|
+
- 字符串字段的补零格式错误;
|
|
76
|
+
- 功能控制区保留位非零;
|
|
77
|
+
- 208 bit 全零保留区存在非零数据。
|
|
78
|
+
|
|
79
|
+
该首包适用于 C2C 直连、反向连接、多跳转发、额外 channel 通道、外部 `C2CSession` 和本地服务探测。未实现该二进制首包的旧节点无法与新协议节点建立连接,因此升级时需要同时更新同一网络中的全部 C2C 节点。
|
|
80
|
+
|
|
81
|
+
协议版本字段固定为16 bit,使用大端序表示无符号整数,取值范围为 `0`–`65535`。客户端收到服务端首包后比较版本:目标节点版本大于或等于本机版本时允许连接,目标节点版本低于本机版本时拒绝连接。例如本机版本为 `4`、目标版本为 `5` 时允许连接;本机版本为 `5`、目标版本为 `4` 时拒绝连接。
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ADP Operator
|
|
2
|
+
|
|
3
|
+
## C2C 连接首包
|
|
4
|
+
|
|
5
|
+
C2C TCP 连接建立后,双方必须先交换固定长度的二进制首包。首包校验通过后,连接才允许继续处理 JSON 业务指令。
|
|
6
|
+
|
|
7
|
+
首包固定为 512 bit,结构如下:
|
|
8
|
+
|
|
9
|
+
| bit 偏移 | bit 长度 | 字段 | 规则 |
|
|
10
|
+
| ---: | ---: | --- | --- |
|
|
11
|
+
| 0 | 24 | 固定标识 | 三个 ASCII 字符 `ADP` |
|
|
12
|
+
| 24 | 16 | 协议版本 | 16-bit 无符号整数,使用大端序,当前版本为 `7` |
|
|
13
|
+
| 40 | 256 | 实例编号 | 当前节点的 `instance_id`,固定32字节 |
|
|
14
|
+
| 296 | 8 | 功能控制区 | bit 0–2为功能开关,bit 3–7为保留位 |
|
|
15
|
+
| 304 | 208 | 全零保留区 | 必须全部为 `0` |
|
|
16
|
+
|
|
17
|
+
字符串字段统一使用 UTF-8 编码。字段长度不足时在尾部使用 `0x00` 填充,超过字段长度时按字段长度截断。
|
|
18
|
+
|
|
19
|
+
### 功能控制区
|
|
20
|
+
|
|
21
|
+
功能控制区固定为8 bit,每个功能占用一个 bit:
|
|
22
|
+
|
|
23
|
+
| bit | 功能 | `0` | `1` | 当前默认值 |
|
|
24
|
+
| ---: | --- | --- | --- | --- |
|
|
25
|
+
| 0 | UDP 主动发现 | 关闭 | 开启 | `1` |
|
|
26
|
+
| 1 | 额外 channel 通道 | 不支持 | 支持 | `1` |
|
|
27
|
+
| 2 | WebSocket 业务端口 | 关闭 | 开启 | `1` |
|
|
28
|
+
| 3–7 | 保留位 | 必须为零 | 不允许 | `0` |
|
|
29
|
+
|
|
30
|
+
当前默认控制区的二进制内容为:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
00000111
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 握手流程
|
|
37
|
+
|
|
38
|
+
1. TCP 客户端连接 C2C 服务端。
|
|
39
|
+
2. 客户端发送自己的 512 bit 二进制首包。
|
|
40
|
+
3. 服务端读取并校验完整首包。
|
|
41
|
+
4. 服务端校验通过后,向客户端返回自己的 512 bit 二进制首包。
|
|
42
|
+
5. 客户端校验服务端首包。
|
|
43
|
+
6. 双方校验均通过后,才开始发送 JSON 业务指令。
|
|
44
|
+
|
|
45
|
+
服务端会拒绝以下连接:
|
|
46
|
+
|
|
47
|
+
- 首包长度不足 512 bit;
|
|
48
|
+
- 固定标识不是 `ADP`;
|
|
49
|
+
- 目标节点协议版本低于发起连接节点的协议版本;
|
|
50
|
+
- `instance_id` 为空;
|
|
51
|
+
- 字符串字段的补零格式错误;
|
|
52
|
+
- 功能控制区保留位非零;
|
|
53
|
+
- 208 bit 全零保留区存在非零数据。
|
|
54
|
+
|
|
55
|
+
该首包适用于 C2C 直连、反向连接、多跳转发、额外 channel 通道、外部 `C2CSession` 和本地服务探测。未实现该二进制首包的旧节点无法与新协议节点建立连接,因此升级时需要同时更新同一网络中的全部 C2C 节点。
|
|
56
|
+
|
|
57
|
+
协议版本字段固定为16 bit,使用大端序表示无符号整数,取值范围为 `0`–`65535`。客户端收到服务端首包后比较版本:目标节点版本大于或等于本机版本时允许连接,目标节点版本低于本机版本时拒绝连接。例如本机版本为 `4`、目标版本为 `5` 时允许连接;本机版本为 `5`、目标版本为 `4` 时拒绝连接。
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
import threading
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from adp_operator.c2c.operator_executor import OperatorExecutor
|
|
10
|
+
from adp_operator.c2c.runtime import Service
|
|
11
|
+
from adp_operator.c2c.operator_loader import load_operator
|
|
12
|
+
from adp_operator.c2c.operator import C2COperatorBase
|
|
13
|
+
from adp_operator.c2c.operator_source import prepare_operator_source
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class C2CClient(Service):
|
|
17
|
+
"""一个进程承载一个算子类,按 info/run 指令加载信息或执行算子。"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
operator_config: dict,
|
|
22
|
+
node_id: Optional[str] = None,
|
|
23
|
+
network: Optional[str] = None,
|
|
24
|
+
max_concurrent_runs: Optional[int] = None,
|
|
25
|
+
join: Optional[tuple[str, int]] = None,
|
|
26
|
+
):
|
|
27
|
+
self.operator_config = dict(operator_config)
|
|
28
|
+
self.operator_base = "PermanentOperator" if join is not None else "ADPOperator"
|
|
29
|
+
source = Path(operator_config["module_file"]).expanduser().resolve()
|
|
30
|
+
self._source_config = {**operator_config, "module_file": str(source)}
|
|
31
|
+
self._workspaces = {}
|
|
32
|
+
self._workspace_lock = threading.RLock()
|
|
33
|
+
identity = hashlib.sha256(
|
|
34
|
+
f"{node_id or operator_config['name']}:{operator_config['module_name']}".encode()
|
|
35
|
+
).hexdigest()
|
|
36
|
+
self._namespace = Path(tempfile.gettempdir()).resolve() / "adp-operator-clients" / identity
|
|
37
|
+
self._namespace.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
38
|
+
self._temporary_directory = tempfile.TemporaryDirectory(prefix="session-", dir=self._namespace)
|
|
39
|
+
self.working_directory = Path(self._temporary_directory.name)
|
|
40
|
+
self._previous_directory = Path.cwd()
|
|
41
|
+
self._import_root = str(self._previous_directory)
|
|
42
|
+
self._added_import_root = self._import_root not in sys.path
|
|
43
|
+
try:
|
|
44
|
+
target = self.working_directory / source.name
|
|
45
|
+
prepare_operator_source(
|
|
46
|
+
source, target, operator_config["name"], self.operator_base
|
|
47
|
+
)
|
|
48
|
+
self.operator_config["module_file"] = str(target)
|
|
49
|
+
if self._added_import_root:
|
|
50
|
+
sys.path.insert(0, self._import_root)
|
|
51
|
+
os.chdir(self.working_directory)
|
|
52
|
+
self.operator_class = load_operator(self.operator_config)
|
|
53
|
+
if not issubclass(self.operator_class, C2COperatorBase):
|
|
54
|
+
raise TypeError(
|
|
55
|
+
f"{self.operator_class.__name__} 必须继承 ADPOperator 或 PermanentOperator"
|
|
56
|
+
)
|
|
57
|
+
resolved_node_id = node_id or operator_config["name"]
|
|
58
|
+
if max_concurrent_runs is None:
|
|
59
|
+
max_concurrent_runs = max(1, (os.cpu_count() or 1) * 3)
|
|
60
|
+
executor = OperatorExecutor(
|
|
61
|
+
self.operator_class, resolved_node_id, max_concurrent_runs=max_concurrent_runs
|
|
62
|
+
)
|
|
63
|
+
executor.operator_config = self.operator_config
|
|
64
|
+
executor.working_directory = self.working_directory
|
|
65
|
+
executor.get_workspace = self.get_workspace
|
|
66
|
+
executor.close_workspace = self.close_workspace
|
|
67
|
+
super().__init__(resolved_node_id, network, executor, join=join)
|
|
68
|
+
except Exception:
|
|
69
|
+
self._cleanup_workspace()
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
def _cleanup_workspace(self):
|
|
73
|
+
for session_id in list(self._workspaces):
|
|
74
|
+
self.close_workspace(session_id)
|
|
75
|
+
if Path.cwd() == self.working_directory:
|
|
76
|
+
os.chdir(self._previous_directory)
|
|
77
|
+
if self._added_import_root and self._import_root in sys.path:
|
|
78
|
+
sys.path.remove(self._import_root)
|
|
79
|
+
self._temporary_directory.cleanup()
|
|
80
|
+
|
|
81
|
+
def get_workspace(self, session_id):
|
|
82
|
+
"""文件接收和算子运行使用同一个会话目录。"""
|
|
83
|
+
with self._workspace_lock:
|
|
84
|
+
if session_id not in self._workspaces:
|
|
85
|
+
if not self._workspaces and self.working_directory.exists():
|
|
86
|
+
temporary = self._temporary_directory
|
|
87
|
+
else:
|
|
88
|
+
temporary = tempfile.TemporaryDirectory(prefix="session-", dir=self._namespace)
|
|
89
|
+
directory = Path(temporary.name)
|
|
90
|
+
try:
|
|
91
|
+
source = Path(self._source_config["module_file"])
|
|
92
|
+
target = directory / source.name
|
|
93
|
+
prepare_operator_source(
|
|
94
|
+
source, target, self._source_config["name"], self.operator_base
|
|
95
|
+
)
|
|
96
|
+
config = {**self._source_config, "module_file": str(target)}
|
|
97
|
+
self._workspaces[session_id] = (temporary, config)
|
|
98
|
+
except Exception:
|
|
99
|
+
temporary.cleanup()
|
|
100
|
+
raise
|
|
101
|
+
return self._workspaces[session_id][1]
|
|
102
|
+
|
|
103
|
+
def close_workspace(self, session_id):
|
|
104
|
+
with self._workspace_lock:
|
|
105
|
+
workspace = self._workspaces.get(session_id)
|
|
106
|
+
if workspace is None:
|
|
107
|
+
return
|
|
108
|
+
temporary, _ = workspace
|
|
109
|
+
if Path.cwd() == Path(temporary.name):
|
|
110
|
+
os.chdir(self._previous_directory)
|
|
111
|
+
temporary.cleanup()
|
|
112
|
+
del self._workspaces[session_id]
|
|
113
|
+
|
|
114
|
+
def start(self, blocking: bool = True) -> None:
|
|
115
|
+
try:
|
|
116
|
+
super().start(blocking=blocking)
|
|
117
|
+
except Exception:
|
|
118
|
+
if self._stopped:
|
|
119
|
+
self._cleanup_workspace()
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def stop(self) -> None:
|
|
123
|
+
super().stop()
|
|
124
|
+
self._cleanup_workspace()
|
|
125
|
+
|
|
126
|
+
def get_extra_info(self):
|
|
127
|
+
return self.executor.get_extra_info()
|
|
128
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""与网络入口无关的 C2C 指令实现。"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def output_event(event, prefix=""):
|
|
10
|
+
text = event.get("message", event)
|
|
11
|
+
lines = str(text).splitlines() or [""]
|
|
12
|
+
if event.get("type") == "log":
|
|
13
|
+
logger = logging.getLogger(event.get("logger") or __name__)
|
|
14
|
+
level = getattr(logging, str(event.get("level", "INFO")).upper(), logging.INFO)
|
|
15
|
+
if not isinstance(level, int):
|
|
16
|
+
level = logging.INFO
|
|
17
|
+
logger.disabled = False
|
|
18
|
+
if logger.getEffectiveLevel() > level:
|
|
19
|
+
logger.setLevel(level)
|
|
20
|
+
if not logger.hasHandlers():
|
|
21
|
+
handler = logging.StreamHandler()
|
|
22
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
|
|
23
|
+
logger.addHandler(handler)
|
|
24
|
+
logger.propagate = False
|
|
25
|
+
logger.log(level, "\n".join(f"{prefix}{line}" for line in lines))
|
|
26
|
+
return
|
|
27
|
+
print("\n".join(f"{prefix}{line}" for line in lines), flush=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class C2CCommands:
|
|
31
|
+
"""统一实现 C2C 的运行、调用和文件指令。"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, transport, session_id: str | None = None) -> None:
|
|
34
|
+
self.transport = transport
|
|
35
|
+
self.session_id = session_id or uuid.uuid4().hex
|
|
36
|
+
self._operators: set[str] = set()
|
|
37
|
+
self._closed = False
|
|
38
|
+
|
|
39
|
+
def can_reach(self, operator_name: str) -> bool:
|
|
40
|
+
return self.transport.can_reach(operator_name, self.session_id)
|
|
41
|
+
|
|
42
|
+
def call(self, operator_name: str, method: str, *args, **kwargs):
|
|
43
|
+
if not self.can_reach(operator_name):
|
|
44
|
+
return None
|
|
45
|
+
result = None
|
|
46
|
+
result_received = False
|
|
47
|
+
for event in self._request(operator_name, {
|
|
48
|
+
"type": "call",
|
|
49
|
+
"operator_name": operator_name,
|
|
50
|
+
"method": method,
|
|
51
|
+
"args": list(args),
|
|
52
|
+
"kwargs": kwargs,
|
|
53
|
+
}, {"call_end", "call_error"}):
|
|
54
|
+
if event.get("type") == "call_error":
|
|
55
|
+
raise RuntimeError(event.get("error", "C2C 方法调用失败"))
|
|
56
|
+
if event.get("type") == "call_result":
|
|
57
|
+
result = event.get("result")
|
|
58
|
+
result_received = True
|
|
59
|
+
if event.get("type") == "call_end":
|
|
60
|
+
if not result_received:
|
|
61
|
+
raise RuntimeError(f"方法 {operator_name}.{method} 未返回结果")
|
|
62
|
+
return result
|
|
63
|
+
raise RuntimeError(f"方法 {operator_name}.{method} 的事件流异常结束")
|
|
64
|
+
|
|
65
|
+
def run(self, message: dict, on_event=None):
|
|
66
|
+
operator_name = message["operator_name"]
|
|
67
|
+
result = None
|
|
68
|
+
for event in self._request(
|
|
69
|
+
operator_name, message, {"run_end", "run_error"}
|
|
70
|
+
):
|
|
71
|
+
event_type = event.get("type")
|
|
72
|
+
if event_type == "log":
|
|
73
|
+
continue
|
|
74
|
+
if event_type == "run_end" and result is None:
|
|
75
|
+
event = {
|
|
76
|
+
"type": "run_error",
|
|
77
|
+
"status": "error",
|
|
78
|
+
"operator_name": operator_name,
|
|
79
|
+
"pipeline_id": message.get("pipeline_id"),
|
|
80
|
+
"error": f"算子 {operator_name} 运行失败,未返回结果",
|
|
81
|
+
}
|
|
82
|
+
event_type = "run_error"
|
|
83
|
+
if event_type in ("run_result", "run_error"):
|
|
84
|
+
result = event
|
|
85
|
+
if on_event is None:
|
|
86
|
+
output_event(event)
|
|
87
|
+
else:
|
|
88
|
+
on_event(event)
|
|
89
|
+
if event_type == "run_error":
|
|
90
|
+
return result
|
|
91
|
+
if event_type == "run_end":
|
|
92
|
+
return result
|
|
93
|
+
raise RuntimeError(f"算子 {operator_name} 的事件流异常结束")
|
|
94
|
+
|
|
95
|
+
def transfer_file(self, operator_name: str, path, filename: str) -> str:
|
|
96
|
+
offset = 0
|
|
97
|
+
response = None
|
|
98
|
+
with Path(path).open("rb") as source:
|
|
99
|
+
for chunk in iter(lambda: source.read(512 * 1024), b""):
|
|
100
|
+
response = self._single(operator_name, {
|
|
101
|
+
"type": "receive_file",
|
|
102
|
+
"operator_name": operator_name,
|
|
103
|
+
"filename": filename,
|
|
104
|
+
"offset": offset,
|
|
105
|
+
"content": base64.b64encode(chunk).decode("ascii"),
|
|
106
|
+
}, "receive_file_ack")
|
|
107
|
+
offset += len(chunk)
|
|
108
|
+
if response.get("size") != offset:
|
|
109
|
+
raise RuntimeError("文件接收确认不一致")
|
|
110
|
+
if response is None:
|
|
111
|
+
response = self._single(operator_name, {
|
|
112
|
+
"type": "receive_file",
|
|
113
|
+
"operator_name": operator_name,
|
|
114
|
+
"filename": filename,
|
|
115
|
+
"offset": 0,
|
|
116
|
+
"content": "",
|
|
117
|
+
}, "receive_file_ack")
|
|
118
|
+
if response.get("size") != 0:
|
|
119
|
+
raise RuntimeError("空文件接收确认不一致")
|
|
120
|
+
return response["path"]
|
|
121
|
+
|
|
122
|
+
def sync_files(self, operator_name: str, targets: list[str]) -> dict:
|
|
123
|
+
targets = list(dict.fromkeys(targets))
|
|
124
|
+
if not targets:
|
|
125
|
+
return {}
|
|
126
|
+
self._operators.update(targets)
|
|
127
|
+
response = self._single(operator_name, {
|
|
128
|
+
"type": "sync_files",
|
|
129
|
+
"operator_name": operator_name,
|
|
130
|
+
"targets": targets,
|
|
131
|
+
}, "sync_files_ack")
|
|
132
|
+
return response["synchronized"]
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
if self._closed:
|
|
136
|
+
return
|
|
137
|
+
error = None
|
|
138
|
+
try:
|
|
139
|
+
for operator_name in self._operators:
|
|
140
|
+
try:
|
|
141
|
+
self._single(operator_name, {
|
|
142
|
+
"type": "close_session",
|
|
143
|
+
"operator_name": operator_name,
|
|
144
|
+
}, "close_session_ack")
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
error = error or exc
|
|
147
|
+
finally:
|
|
148
|
+
self._closed = True
|
|
149
|
+
self.transport.close()
|
|
150
|
+
if error is not None:
|
|
151
|
+
raise error
|
|
152
|
+
|
|
153
|
+
def _single(self, operator_name: str, message: dict, response_type: str) -> dict:
|
|
154
|
+
response = None
|
|
155
|
+
for event in self._request(operator_name, message, {response_type}):
|
|
156
|
+
response = event
|
|
157
|
+
if response is None or response.get("type") != response_type:
|
|
158
|
+
raise RuntimeError(f"{message['type']} 响应无效")
|
|
159
|
+
if response.get("status") == "error":
|
|
160
|
+
raise RuntimeError(response.get("error", "C2C 请求失败"))
|
|
161
|
+
return response
|
|
162
|
+
|
|
163
|
+
def _request(
|
|
164
|
+
self,
|
|
165
|
+
operator_name: str,
|
|
166
|
+
message: dict,
|
|
167
|
+
terminal_types: set[str],
|
|
168
|
+
):
|
|
169
|
+
self._operators.add(operator_name)
|
|
170
|
+
yield from self.transport.request(
|
|
171
|
+
operator_name,
|
|
172
|
+
message,
|
|
173
|
+
self.session_id,
|
|
174
|
+
terminal_types,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def __enter__(self):
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
181
|
+
self.close()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
BROADCAST_BASE_PORT = 50000
|
|
2
|
+
BROADCAST_PORT_RANGE = 1000
|
|
3
|
+
SERVICE_BASE_PORT = 60000
|
|
4
|
+
SERVICE_PORT_RANGE = 1000
|
|
5
|
+
WEBSOCKET_BASE_PORT = 61000
|
|
6
|
+
WEBSOCKET_PORT_RANGE = 1000
|
|
7
|
+
BROADCAST_INTERVAL = 3
|
|
8
|
+
PEER_SYNC_INTERVAL = 3
|
|
9
|
+
PEER_TTL = 300
|
|
10
|
+
MAX_REQUEST_SIZE = 1024 * 1024
|
|
11
|
+
|