Files
chongming-core/docs/process-plugin.md
T

21 KiB
Raw Blame History

进程插件开发指南(external 模式)

进程插件(Process Plugin)是运行在独立进程中的 Worker 插件(load_mode = "external"), 与网关零导入关系、仅经 Zenoh 通讯,可独立打包部署到不同容器。 本文档覆盖从创建代码 → 配置 → 调试 → 验证 → 打包 → 部署的完整开发流程。


1. 什么是进程插件

网关支持两种插件运行模式,按插件粒度共存(混合部署):

模式 运行位置 目录 路由来源 契约校验位置
inline(默认) 网关进程内 import src/plugins/<name>/(chongming-plugins 仓库) 网关盘扫 TOML 生成 网关启动时
external(进程插件) 独立进程 workers/<name>/(本地开发目录,自包含项目,不入库) Worker 经 Zenoh 发布路由描述符,网关动态挂载 Worker 进程内

进程插件的特点:

  • 零导入关系:只依赖 chongming-worker SDK + zenoh,禁止 import chongming(网关包)
  • 独立生命周期:由 Docker/K8s 编排拉起,进程入口 chongming-worker --type <name>(SDK console script)
  • 动态路由:启动时把路由描述符发布到 Zenoh chongming/routes/<type>/...,网关 RouteRegistry 常驻订阅后动态挂载 HTTP 端点;退出时摘除
  • 独立打包:uv run build_plugin <name> 一键打包为可部署 zip,与网关分别部署到不同容器
  • 代码结构一致:与 inline 插件完全相同(Plugin 基类 + router/ + models/)

适用场景:需要独立扩缩容、强隔离(第三方依赖冲突、资源占用大)、或与网关发布节奏解耦的业务。


2. 工作机制(简述)

graph TB
    HTTP[HTTP 客户端] -->|REST API| API[FastAPI 动态路由]
    REG[网关 RouteRegistry<br/>订阅 chongming/routes/**] -->|动态挂载/摘除| API
    API --> WM[WorkerManager<br/>负载均衡 + 超时重试]

    subgraph GW[网关容器]
        API
        REG
        WM
    end

    subgraph PW[进程插件容器 workers/demo]
        WS[chongming-worker 进程]
        WS -->|① put 路由描述符<br/>chongming/routes/demo/**| REG
        WS -->|② liveliness 注册<br/>worker/demo/<id>| WM
    end

    WM -->|Zenoh Query| WS
    WS -.->|每 10s 重发布(自愈)| REG
    WS -.->|SIGTERM 时 delete 摘除| REG

chongming-worker --type demo 启动后的流程:

  1. 读根 config.toml 的 [zenoh] 节打开会话,自读 config/workers/demo/config.toml 的 [workers] 节(含 [workers.config] 运行时配置)
  2. import 插件模块触发 @rpc_method 注册 → 进程内契约校验(不通过则退出码 1)
  3. create_workers 创建副本(默认 count 个,--count 可覆盖),注册 liveliness
  4. 经 Zenoh 把路由描述符 put 到 chongming/routes/<type>/<route_id>,网关动态挂载 HTTP 路由; 每 10s 重发布(网关后启动或样本丢失时自愈)
  5. 收到 SIGTERM/SIGINT → delete 描述符(路由摘除)→ cleanup → 关闭会话

要点:网关容器无需放置 external 插件的配置文件(RouteRegistry 常驻订阅,与盘扫解耦); 描述符字段与 keyexpr 细节见 线协议规范。


3. 目录结构约定

进程插件是自包含项目:workers/<name>/ 即项目根,同时也是打包 zip 的根 (gitignore,不入库)。开发目录与容器工作目录完全一致,开发即产物:

workers/<name>/                      # 项目根 = zip 根 = 容器工作目录
├── workers/<name>/                  # 插件代码(模块 workers.<name>)
│   ├── __init__.py                  # Plugin 子类 + create_workers / cleanup 模块级入口
│   ├── global_var.py                # 模块级共享状态(Database、API client 等)
│   ├── router/                      # RPC 端点实现
│   │   ├── __init__.py              # 导入所有端点模块
│   │   └── <操作名>.py              # 每个端点一个文件,@rpc_method 装饰器
│   ├── models/                      # 数据模型(gen_models 自动生成,勿手动修改)
│   │   ├── __init__.py
│   │   └── pydantic.py              # 请求/响应 Pydantic 模型
│   └── README.md                    # 插件说明(可选)
├── config/
│   ├── zenoh_config.json5           # Zenoh 会话配置
│   └── workers/<name>/              # 插件注册 + 端点配置
│       ├── config.toml              # [workers] 节注册 + [worker] 元信息 + [workers.config]
│       └── <操作名>.toml            # 每个端点一个定义文件
├── config.toml                      # 最小根配置(仅 [zenoh] 节,worker 启动必需)
├── pyproject.toml                   # uv 依赖清单(chongming-worker + 扩展依赖)
└── start.sh                         # 容器入口(uv sync + uv run chongming-worker --type <name>)

参考示例:workers/demo/(完整项目,含 5 个端点)。


4. 开发流程

第 1 步:生成项目骨架

uv run gen_plugin demo --process                # 生成完整进程插件项目(含示例操作)
uv run gen_plugin demo hello storage --process  # 指定初始操作
uv run gen_plugin demo add --process            # 为已有项目增量添加操作

生成的代码位于 workers/demo/workers/demo/,其中 __init__.py(与 inline 插件 完全一致,推荐 Plugin 基类风格):

import logging

from chongming_worker.plugin import Plugin

from . import router  # noqa: F401  触发 @rpc_method 注册

logger = logging.getLogger(__name__)


class DemoPlugin(Plugin):
    type = "demo"
    db_name = None        # 不需要数据库则置 None
    auto_bind = True

    async def on_init(self):
        """首次 create_workers 时调用一次:初始化资源。"""
        # self.set_var("db", self.db)
        pass

    async def on_cleanup(self):
        """cleanup 时调用:关闭自定义客户端等。"""


_plugin = DemoPlugin()
create_workers = _plugin.create_workers
cleanup = _plugin.cleanup

注意:module 注册为 workers.<name> 时,sys.path 需包含项目根目录—— chongming-worker 已自动处理(console script 启动时会显式把 CWD 加入 sys.path), 只需在项目根 workers/<name>/ 下运行。

第 2 步:插件注册配置

创建 workers/<name>/config/workers/<name>/config.toml,[workers] 节注册插件 (关键:load_mode = "external"):

# workers/demo/config/workers/demo/config.toml
[workers]
type = "demo"                  # 插件类型(worker type)
module = "workers.demo"        # Python 模块路径(进程插件用 workers.<name>)
factory = "create_workers"     # 工厂函数名(Plugin 基类绑定的方法)
cleanup = "cleanup"            # 清理函数名
count = 2                      # Worker 副本数
load_mode = "external"         # ★ 进程插件:独立进程运行,经 Zenoh 注册路由

[worker]
name = "demo"
version = "0.1.0"
description = "进程插件示例"
defaults = { method = "GET", timeout = 30.0, auth_required = false, cacheable = false, internal = false }

[worker.registration]
router_prefix = "/demo"        # HTTP 路由前缀
tags = ["demo", "process-plugin"]

# Worker 运行时配置(展平后传给 create_workers 的 config 参数)
[workers.config]
greeting_prefix = "Hello"

运行时配置的来源(两种模式):[workers.config] 与 count 在启动时按 [workers.cache].launch_worker_count 取值——"toml"(默认)以本文件为准,并把该段覆盖 写入共享缓存(网关 GET /admin/workers/<type>/config 读缓存,因此前端显示文件值); "cache" 以缓存为准(插件初始化即读缓存值,管理接口改动跨重启保持,本文件仅在缓存缺失时 引导)。进程插件未配置 [workers.cache] 时无缓存句柄,恒为 toml 语义,且与网关内同名 副本可能读到不同来源——需要与网关共享"改完即持久"的语义时请补上与网关一致的 [workers.cache]。

第 3 步:端点 TOML 定义

每个端点一个文件,如 workers/<name>/config/workers/<name>/hello.toml:

[GET]
summary = "进程插件示例:问候"
params = ["name: str"]
auth_required = false

[GET.response_model]
greeting = { type = "str", required = true }
worker_id = { type = "str", required = false, default = "" }
timestamp = { type = "float", required = true }

流式端点(SSE)在方法节追加 stream = true 并配大 timeout,详见主 README「流式端点」。

第 4 步:生成 Pydantic 模型

uv run gen_models workers/demo/config/workers/demo workers/demo/workers/demo

在 workers/<name>/workers/<name>/models/ 下生成 pydantic.py 与 __init__.py。

第 5 步:实现 RPC 端点

workers/<name>/workers/<name>/router/hello.py:

import time

from chongming_worker.decorators import rpc_method

from ..models.pydantic import HelloGetRequest, HelloGetResponse


@rpc_method(subject="demo.hello", method="GET", response_model=HelloGetResponse)
async def hello(request: HelloGetRequest) -> HelloGetResponse:
    return HelloGetResponse(
        greeting=f"Hello, {request.name}!",
        timestamp=time.time(),
    )

workers/<name>/workers/<name>/router/__init__.py:

from . import hello

__all__ = ["hello"]

异常语义(插件作者只需 return / raise,通道由 SDK 处理):

形态 插件写法 网关行为
业务错误 return {"code": 1, "error": "..."} HTTP 200 透传(业务语义,网关不解析)
框架异常(非流式) raise ValueError(...) SDK 经 reply_err 通道回复 → 网关重试后 HTTP 503
流式异常 生成器内 raise SDK 发送 {"error": ...} 收尾块 → SSE error 事件终止

即「返回即业务、抛出即框架」。注意:框架异常从历史行为「HTTP 200 + 错误信封」 修正为 503(reply_err 通道,见 wire 协议 §4.3 兼容矩阵)。

第 6 步:本地调试

在插件项目根 workers/<name>/ 下运行(worker 以 CWD 为工作目录读配置/import 代码):

cd workers/demo
uv sync                                  # 首次:安装依赖(项目独立 venv,与容器一致)
uv run chongming-worker --type demo      # 本地调试
uv run chongming-worker --type demo --count 1   # 覆盖副本数
./start.sh                               # 与容器一致的启动方式
# 开发期复用仓库 venv(已装好 SDK)时,用 --project 指向仓库根:
uv run --project .. chongming-worker --type demo

启动后预期日志:

[demo] 路由发布完成: 5 端点 + 无元信息
[demo] Worker 进程就绪,等待退出信号...

若配置了 load_mode 非 "external",会输出告警:网关也会在进程内加载该插件, 外部进程将与网关内副本重复服务。

第 7 步:联调验证

启动网关(uv run serve)后调用插件路由即可验证端到端:网关日志可见路由动态挂载, worker 退出后路由自动摘除(请求变 404),重启 worker 后恢复。

第 8 步:打包

uv run build_plugin demo                    # → dist/demo.zip
uv run build_plugin demo --output /tmp/pkg  # 指定输出目录

打包 = 项目根直接压缩(开发目录即产物,zip 根即容器工作目录),自动排除 __pycache__ / *.pyc / .venv / *.egg-info / .git,start.sh 权限强制 0o755。

产物布局与 §3 完全一致(zip 根即容器工作目录):

workers/<name>/                插件代码(模块 workers.<name>)
config/workers/<name>/         插件注册 + 端点配置
config/zenoh_config.json5      Zenoh 会话配置
config.toml                    最小根配置(仅 [zenoh] 节,worker 启动必需)
pyproject.toml                 uv 依赖清单(chongming-worker 内网 git 源 + orjson + 扩展依赖)
start.sh                       容器入口:uv sync --no-dev + uv run chongming-worker --type <name>

打包前校验(任一不满足即失败退出):

  • workers/<name>/ 项目根存在
  • workers/<name>/config/workers/<name>/config.toml 存在且含 [workers] 节
  • workers/<name>/config/zenoh_config.json5 与 workers/<name>/config.toml(含 [zenoh])存在
  • load_mode 非 "external" 时告警(不阻断,但会造成重复服务)

扩展依赖:直接在项目根 pyproject.toml 的 dependencies 中追加(如 "minio")。

第 9 步:容器部署

# 镜像需预装 uv(https://docs.astral.sh/uv/)
unzip demo.zip -d /app
cd /app
./start.sh
  • start.sh 自动执行 uv sync --no-dev(按 pyproject 从内网 git 源拉取 chongming-worker)并启动 worker
  • 生产部署时进程插件与网关分别打包到不同容器:
    • 网关容器:chongming + 依赖
    • 进程插件容器:插件代码 + chongming-worker + 依赖
  • 插件配置随 zip 部署在插件容器(worker 自读);网关侧无需放置 (RouteRegistry 常驻订阅 Zenoh 路由描述符,与盘扫解耦)

5. 文件传输:直连 S3/Minio 对象存储(不依赖网关)

进程插件可直接对接 S3/Minio 对象存储做文件传输——上传、下载、删除、预签名 URL 均由插件自己的配置([workers.config.storage])驱动,数据路径不经过网关 (预签名 URL 模式下网关甚至不参与数据传输:设备/浏览器拿到 URL 后直传/直取 Minio)。

5.1 配置

# workers/<name>/config/workers/<name>/config.toml
[workers.config.storage]
endpoint = "192.168.1.10:9000"   # Minio 地址 host:port(不带协议前缀)
access_key = "minioadmin"
secret_key = "minioadmin"
bucket = "chongming-files"
secure = false                   # false=http(默认),true=https
region = ""                      # 可选
auto_create_bucket = true        # 可选,默认 true:启动时确保 bucket 存在

语义(chongming_worker.storage.StorageClient.from_config):

情形 行为
未配置 / enabled = false 存储关闭(self.storage = None),端点自行降级
配置段存在但缺必填键(endpoint/access_key/secret_key/bucket) 启动硬失败(ValueError),快速反馈配置错误
连接失败 / bucket 不可用 告警降级为 None(不影响其他端点)

5.2 SDK 用法

基类在 _bootstrap 阶段检测到 config["storage"] 段即自动初始化 self.storage(无需任何额外代码);插件在 on_init 发布到 global_var 供路由读取:

class DemoPlugin(Plugin):
    async def on_init(self):
        if self.storage is not None:      # 配置了 storage 段才初始化
            self.set_var("storage", self.storage)

路由内使用(同步核心 + async 包装,RPC handler 直接 await):

from ..global_var import require

storage = require("storage")
await storage.aput_bytes("dir/file.txt", data)          # 上传字节
await storage.aput_file("dir/file.txt", "/tmp/a.txt")   # 上传本地文件
data = await storage.aget_bytes("dir/file.txt")         # 下载字节
await storage.aget_file("dir/file.txt", "/tmp/b.txt")   # 下载到本地文件
url = await storage.apresigned_put_url("dir/file.txt")  # 预签名 PUT(设备直传)
url = await storage.apresigned_get_url("dir/file.txt")  # 预签名 GET(设备直取)
await storage.adelete("dir/file.txt")                   # 删除

API 一览(chongming_worker.storage.StorageClient):

方法(async 同名加 a 前缀) 说明
put_file(object_key, local_path, content_type=None) 上传本地文件(fput_object)
put_bytes(object_key, data, content_type=None) 上传内存字节
get_file(object_key, local_path) 下载到本地文件
get_bytes(object_key) 下载为字节
delete(object_key) 删除对象
presigned_put_url(object_key, expires=3600) 预签名 PUT URL(默认 1 小时)
presigned_get_url(object_key, expires=3600) 预签名 GET URL(默认 1 小时)

5.3 参考示例

workers/demo 演示了 4 个文件传输端点(项目内 config/workers/demo/ 下对应 TOML):

  • POST /demo/presign_upload — filename → object_key + 预签名 PUT URL(设备直传)
  • GET /demo/presign_download — object_key → 预签名 GET URL(设备直取)
  • POST /demo/upload_bytes — base64 小文件经 Zenoh RPC 上传(大文件请用预签名直传)
  • GET /demo/download_bytes — base64 小文件经 Zenoh RPC 下载

配置后重启 worker 即可用;未配置时这些端点返回「storage 未配置」错误(降级演示)。

依赖:minio SDK 随 chongming-worker 一起安装(utils/worker pyproject 已声明), 进程插件无需额外依赖。SDK 侧 chongming_worker.storage 不依赖网关包, 隔离性约束与打包流程均不受影响。


6. 隔离性约束

进程插件禁止 import chongming(网关包),只允许依赖:

  • chongming-worker(worker SDK:chongming_worker.*)
  • zenoh / pydantic 等第三方库
  • 插件自身的扩展依赖(项目根 pyproject.toml 中声明)

隔离性由框架保证:SDK 导入链路中一旦出现 chongming / chongming.* 的 import, 运行时立即 ImportError(启动即暴露),无需等到部署阶段。


7. 进程内契约校验

Worker 进程启动时自读 TOML 并在自己进程内做三方一致性校验 (chongming_worker.contracts.validate_worker_contracts),校验失败直接退出:

检查项 策略
TOML HTTP 路由缺 @rpc_method 硬失败,进程退出
[MQTT] / internal 路由缺注册 警告日志
已注册方法无 TOML 路由 警告日志(反向漂移检测)
params/response_model 缺生成模型 警告日志(gen_models 过期)
生成模型未被任何路由引用 警告日志(孤儿模型)

8. 常见问题排查

现象 原因 / 处理
启动告警「load_mode 不是 external」 进程插件必须在 workers/<name>/config/workers/<name>/config.toml [workers] 节设置 load_mode = "external",否则网关内副本与外部进程重复服务
契约校验失败、进程退出 检查 TOML 端点(subject/方法节)与 @rpc_method(subject=..., method=...) 是否一致;模型缺失时重跑 uv run gen_models workers/<name>/config/workers/<name> workers/<name>/workers/<name>
TypeError: rpc_method() got an unexpected keyword argument 'stream' venv 里是旧版 chongming-worker(git 源拉取),本地未 push 的框架改动不生效;联调时 uv pip install -e utils/worker
路由没出现 worker 是否就绪(日志「路由发布完成」);网关 RouteRegistry 是否订阅(启动流程 5.1);每 10s 自愈重发布,稍等或重启 worker
路由一直 404 worker 是否已退出(优雅退出会摘除路由);进程被 SIGKILL 时描述符残留,重启 worker 后重发布自愈覆盖
找不到插件配置 chongming-worker 须在插件项目根 workers/<name>/ 下运行(读 config/workers/<name>/config.toml,--config 可指定全局配置路径)
storage.* 端点报「storage 未配置」 插件 config.toml 未配置 [workers.config.storage](或 enabled = false / 连接失败降级)。补齐配置后重启 worker;启动日志可见「对象存储已就绪」或「storage 连接失败」告警
容器内 import workers.demo 失败 确认在容器工作目录(zip 根)启动,start.sh 内含 cd "$(dirname "$0")",启动入口会把 CWD 加入 sys.path

9. 相关命令速查

uv run gen_plugin <name> --process               # 生成进程插件项目骨架
cd workers/<name>                                # 以下均在插件项目根下执行
uv run chongming-worker --type <name>             # 本地调试独立 Worker 进程(项目根下)
./start.sh                                       # 与容器一致的启动方式(uv sync + 启动)
cd .. && uv run build_plugin <name>              # 进程插件打包 → dist/<name>.zip
uv run gen_models workers/<name>/config/workers/<name> workers/<name>/workers/<name>   # 生成模型

10. 参考