Files
chongming-core/docs/rust-plugin.md
T
2026-08-31 16:34:13 +08:00

281 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rust 插件编写说明
> 面向「用 Rust 编写 chongming 进程插件」的开发指引,基于 `chongming-worker-rs` SDK。
> 配套:[线协议规范(唯一契约)](./plugin-wire-protocol.md)、[多语言演进规划](./multi-language-plugin-plan.md)、
> [Python 进程插件开发指南](./process-plugin.md)、[SDK 说明](../utils/worker-rs/README.md)。
**先决条件**:Rust 工具链(`cargo` ≥ 1.70)。Rust 插件以**独立进程**运行
(`load_mode = "external"`),与网关零导入关系,仅经 Zenoh 线协议通讯。
---
## 1. 脚手架:`gen_plugin --rust`
推荐直接用脚手架生成自包含项目模板(等价 `workers/rsdemo` 的最小结构):
```bash
uv run gen_plugin myplugin --rust # → workers/myplugin/
uv run gen_plugin myplugin analyze count --rust # 指定初始操作
```
生成内容(config 自包含在插件根,与 Python 进程插件一致;工程化分层,对位 `workers/rsdemo`):
```text
workers/myplugin/
├── config.toml # 最小根配置(仅 [zenoh].json5_path,worker 启动必需)
├── config/
│ ├── zenoh_config.json5 # Zenoh 会话配置(相对 config.toml 解析为绝对路径)
│ └── workers/myplugin/
│ ├── config.toml # [workers] 注册 + [worker] 分组 + defaults + cache/database
│ └── <op>.toml # 每个操作的端点定义(唯一事实来源)
├── Cargo.toml # path 依赖 chongming-worker-rs;features: redis-cache / database
├── src/
│ ├── lib.rs # 库层:声明各模块 + 文档
│ ├── main.rs # 入口(薄):CLI 解析 + 日志 + runtime + App::init + serve + dispose
│ ├── args.rs # clap 命令行参数(--type/--count/--config/--config-dir/--instance)
│ ├── locator.rs # 插件根自定位 plugin_root() + 配置路径解析(Locator)
│ ├── util.rs # 通用小工具 now_ts / deser(含单测)
│ ├── app.rs # 装配 App:数据库生命周期 init/dispose + 端点注册 + 模型指纹校验
│ ├── generated_models.rs # gen_models 生成(勿手动修改)
│ └── handlers/<op>.rs # 每个端点的业务实现(每端点一文件)
├── build.sh # 一键打包(构建 release + 打包插件根含配置)
├── README.md / .gitignore
```
也可以手动复用现有参考插件 `workers/rsdemo/` 作为模板(更完整,含流式、数据库示例)。
---
## 2. 定义端点(TOML 是唯一事实来源)
编辑 `config/workers/<type>/<op>.toml`,格式与 Python 插件**完全一致**
(见 [进程插件开发指南](./process-plugin.md) `[GET]` / `[POST]` / `[GET.response_model]` 约定)。
```toml
# config/workers/myplugin/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 }
```
> 端点名 `<op>.toml` 决定线协议 subject:`myplugin.hello` 与 HTTP 路径 `/myplugin/hello`。
---
## 3. 生成模型
模型由端点 TOML **自动生成**,命名/类型映射对齐 Python(`hello`+`GET` → `HelloGetRequest`):
```bash
cargo run --manifest-path utils/worker-rs/Cargo.toml --bin gen_models -- \
--types myplugin --config-dir workers/myplugin/config/workers \
--out workers/myplugin/src/generated_models.rs
```
类型映射:`str→String` / `int→i64` / `float→f64` / `bool→bool` / `any→Value` /
`list[X]→Vec<X>` / `dict[K,V]→HashMap<K,V>`;`required=false` → `Option<T>`(空值省略)。
**一致性保证(强于 Python)**:启动时 `check_models_fingerprint` 比对 TOML 指纹,
改 TOML 未重跑 `gen_models` 即告警并提示重跑,杜绝模型漂移。
---
## 4. 实现 handler(src/main.rs)
```rust
use chongming_worker_rs::prelude::*;
use chongming_worker_rs::{HandlerRegistry, ServeOptions};
mod generated_models; use generated_models as models;
// 普通(非流式)端点
async fn hello(request: Request) -> Result<Reply, String> {
let req: models::HelloGetRequest =
serde_json::from_value(request.data).map_err(|e| format!("参数解析失败: {e}"))?;
Ok(Reply::Json(serde_json::to_value(&models::HelloGetResponse {
greeting: format!("Hello, {}!", req.name),
worker_id: Some(request.worker_id), // 框架注入副本 ID
timestamp: now_ts(),
}).map_err(|e| e.to_string())?))
}
// 流式端点(SSE 逐块)
async fn stream(request: Request) -> Result<Reply, String> {
let stream = async_stream::stream! {
for i in 0..5 { yield Ok(json!({"index": i})); }
};
Ok(Reply::Stream(Box::pin(stream)))
}
```
在 `main()` 中注册并 serve(脚手架已搭好,main.rs 内含**插件根自定位** `plugin_root()`,
从任意 CWD/部署路径运行都能命中自身 `config.toml`):
```rust
let opts = ServeOptions { config_path, config_dir, count, instance };
tokio::runtime::Builder::new_multi_thread().enable_all().build()?.block_on(async {
let mut registry = HandlerRegistry::new();
registry.register("myplugin.hello", "GET", hello);
registry.register("myplugin.stream", "POST", stream);
chongming_worker_rs::serve::serve(&type_, Arc::new(registry), opts).await
})
```
| CLI 参数 | 说明 |
| ---- | ---- |
| `--type` | 插件类型(= `config/workers/<type>/`) |
| `--count` | 副本数,可覆盖 `[workers].count` |
| `--config` | 全局 `config.toml` 路径(读取 `[zenoh]` 节,默认 `config.toml`) |
| `--instance` | 实例 id(多实例分摊;缺省 `{hostname}:{pid}`) |
生命周期(`serve` 内部自动完成):发布路由描述符 → 声明 liveliness token →
声明业务 + `__admin__` Queryable → 每 10s 重发布(自愈)→ SIGTERM/SIGINT 优雅摘除。
在此之外需要资源初始化/清理时,把 `Arc<Database>` 之类的资源捕获进 handler 闭包,
在 `serve` 返回后做 dispose(见 §7)。
---
## 5. 配置三件套
### 5.1 运行配置 `[workers.config]`
`config/workers/<type>/config.toml` 的 `[workers.config]` 段会被展平传给你。
端点 handler 一般不需要自己读它——需要时用 `chongming_worker_rs::config::load_plugin_config`
取 `runtime` 获取。
### 5.2 多实例共享缓存 `[workers.cache]`
多实例部署(多进程/多机)**必须**启用 Redis 共享缓存,使副本数与配置跨实例收敛为单一事实来源:
```toml
[workers.cache]
backend = "redis" # 仅支持 redis(Python 本地 diskcache 格式 Rust 不可读)
url = "redis://127.0.0.1:6379/0"
launch_worker_count = "cache" # 缓存为 count 单一事实来源
```
编译启用:`bash build.sh dist "redis-cache"`(或 Cargo 依赖加 `chongming-worker-rs/redis-cache`)。
未配置时单实例直接读写本地 `config.toml`(默认无缓存),行为一致。
### 5.3 数据库 `[workers.config.database]`(可选)
见 §7。
---
## 6. 管理指令(admin)
每副本注册 `worker/<type>/<id>/__admin__` Queryable,收到信封 `{"meta":{}, "data":{"cmd":...}}` 分派执行:
| 指令 | 作用 |
| ---- | ---- |
| `status` | 副本状态(epoch/index/status/worker_id/instance_id) |
| `restart` / `stop` / `drain` | 重启 / 停止 / 优雅排空(隔离新请求 + 等在途归零,30s 超时) |
| `get_config` / `config_set` | 读 / 改 `[workers.config]`(保留键校验 + 原子落盘 + 热更重启) |
| `rescale` | 调整副本数(多实例按缓存全局分摊) |
可经网关 `/admin/workers/<type>/...` 端点或直接对 `worker/<type>/<id>/__admin__`
发 Zenoh Query 触发。开发者无需改任何代码,SDK 已内置。
---
## 7. 数据库接入(对齐 Python `chongming_database`)
Rust 用 `chongming-worker-rs` 的 `database` 模块(sqlx)获得与 Python
`chongming_database` 等价的能力:SQLite / PostgreSQL 异步连接池 + 生命周期。
- 声明后端(在 `[workers.config].database`,缺省/未配置则插件不建库,对齐 `db_name = None`):
```toml
[workers.config.database]
type = "sqlite" # "sqlite"(默认) | "postgresql"
database_path = "./data/myplugin.db"
# postgresql → type = "postgresql"; url = "postgres://user:pwd@host:5432/db"
```
- 初始化并把 `Arc<Database>` 捕获进 handler(`main.rs`):
```rust
#[cfg(feature = "database")]
let db = chongming_worker_rs::database::init_database(
&type_, &config_dir,
Some("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL)"),
).await?.map(Arc::new); // Option<Arc<Database>>
#[cfg(feature = "database")]
if let Some(db) = db.clone() {
registry.register("myplugin.db_ping", "GET", move |req| { let db = db.clone(); Box::pin(async move {
let pool = db.pool().await?;
sqlx::query("UPDATE ...").execute(&pool).await.map_err(|e| e.to_string())?;
Ok(Reply::Json(json!({"ok": true})))
})});
}
```
- 依赖:Cargo.toml 开启 `database` feature(脚手架已内置:`database = ["chongming-worker-rs/database", "sqlx"]`),
打包 `bash build.sh dist "database"`。
- 差异点:SQLite 连接串 `sqlite:<path>?mode=rwc`(`rwc` 自动建库),PostgreSQL `postgres://...`;
SQL 占位符遵循各自方言(SQLite `?`、PostgreSQL `$1`);默认不启用 TLS,PostgreSQL 走 SSL 需追加
`tls-rustls-ring` 等 feature。参考实现见 `workers/rsdemo` 的 `db_ping`。
---
## 8. 打包与部署
在**插件根目录**执行(配置已自包含,打包脚本会把 release 二进制 + 全部配置带上):
```bash
cd workers/myplugin
bash build.sh # → dist/myplugin.zip
bash build.sh dist "redis-cache" # 需要多实例共享缓存时
bash build.sh dist "database" # 需要数据库(sqlx)时
```
产物 zip 根即容器工作目录:含 release 二进制 + `config.toml` + `config/` + `start.sh`,
**解压即运行**(`./start.sh`,无需 uv / Python 运行时)。容器部署:
```bash
unzip myplugin.zip -d /app && cd /app && ./start.sh
```
- `load_mode = "external"` 必须保留:网关跳过进程内加载,路由完全由描述符驱动。
- 与 Python 插件**可同 type 混布**:描述符 hash 一致 → 网关幂等去重零冲突,负载均衡透明分发。
---
## 9. 本地验证
```bash
# Zenoh Router(替代 zenohd)
.venv/bin/python utils/worker-rs/scripts/zenoh_router.py &
# 协议级 15 项断言(描述符/RPC/流式/错误/摘除/自愈)
.venv/bin/python utils/worker-rs/scripts/verify_e2e.py \
--type myplugin --bin workers/myplugin/target/debug/myplugin
```
---
## 10. 与 Python SDK 的差异(有意为之)
| 差异 | 说明 |
| ---- | ---- |
| 契约校验 | Python 进程内三方校验(硬失败);Rust 为**发布前自检**:无 handler 端点告警跳过,不发布残缺路由 |
| 模型生成 | Rust 用 gen_models 生成强类型结构体 + **启动时指纹校验**(TOML 变更未重生成即告警,比 Python 更强) |
| handler 注入 | worker_id 经 `Request.worker_id`、user_info 经 `Request.user_info` 注入(非函数参数) |
| 配置缓存后端 | Rust 仅 Redis(无法读 Python 本地 diskcache);单实例走无缓存回退 |
| 数据库 | Rust 走 sqlx(见 §7),配置键与 Python `chongming_database` 对齐 |
---
## 11. 参考材料
- 线协议规范:`./plugin-wire-protocol.md`(唯一契约,[附录 A 事实来源索引](./plugin-wire-protocol.md))
- 多语言演进规划:`./multi-language-plugin-plan.md`(含数据库/缓存对齐表)
- 完整示例插件:`../workers/rsdemo/`(流式、错误、数据库、管理指令全覆盖)
- 脚手架:`uv run gen_plugin --help`(`--rust`)