314 lines
14 KiB
Markdown
314 lines
14 KiB
Markdown
# 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]`
|
||
|
||
Rust 进程插件支持两种共享缓存后端(schema 与 Python `chongming-cache` 完全一致):
|
||
|
||
1. **本机共享 `sqlite`(feature `local-cache`)**:同一份 SQLite 文件由 Python 与 Rust 读写,
|
||
实现单机多实例/跨语言收敛,无需 Redis。需 `cargo build --features local-cache`:
|
||
```toml
|
||
[workers.cache]
|
||
backend = "sqlite" # schema 与 Python SqliteBackend 一致(旧名 diskcache 兼容)
|
||
path = "./cache_data/cache.db" # 与网关该实例 `[cache].path` 指向同一文件
|
||
launch_worker_count = "cache"
|
||
```
|
||
2. **跨主机 `redis`(feature `redis-cache`)**:多机部署必须用 Redis 共享缓存:
|
||
```toml
|
||
[workers.cache]
|
||
backend = "redis"
|
||
url = "redis://127.0.0.1:6379/0"
|
||
prefix = "chongming:" # 与网关 config.toml [cache].redis_prefix 对齐(缺省空)
|
||
launch_worker_count = "cache"
|
||
```
|
||
未指定 backend(缺省)时 Rust 走 `Cache::None`,退化为本地 toml 语义。
|
||
|
||
> `prefix` 必须与网关 `[cache].redis_prefix`(如 `"chongming:"`)一致,Rust 插件才会读写
|
||
> 与网关同一批共享键。若网关未设前缀可省略。缓存值统一以 JSON 存储(Python 侧用
|
||
> orjson、Rust 侧用 serde_json),**任意键**均可跨语言互通;与 Rust 共享的键请使用
|
||
> JSON 兼容类型(dict/list/int/str/bool/null)。
|
||
|
||
编译启用:`bash build.sh dist "redis-cache"`(或 Cargo 依赖加 `chongming-worker-rs/redis-cache`)。
|
||
未配置时单实例直接读写本地 `config.toml`(默认无缓存),行为一致。
|
||
|
||
#### 5.2.1 配置优先来源模式(`launch_worker_count`)
|
||
|
||
`[workers.cache].launch_worker_count` 与网关根 `config.toml` 的同名键语义一致,决定启动时
|
||
`[workers.config]` 与 count 从哪一侧取值(SDK 的 `config::resolve_runtime_config` /
|
||
`cache::resolve_worker_count` 按同一规则实现,与 Python SDK 对位):
|
||
|
||
| 环节 | `"toml"`(默认) | `"cache"` |
|
||
| ---- | ---------------- | --------- |
|
||
| 启动时 | 以 `config/workers/<type>/config.toml` 为准,并把该段**覆盖写入缓存**(网关读缓存 → 显示 toml 值) | 以缓存为准,插件初始化即读缓存值;缓存缺失才用 toml 引导并写回 |
|
||
| 插件实际使用 | toml 值 | 缓存值(跨重启保持) |
|
||
| `config_set` 改动的持久性 | 仅本次运行有效,下次启动被文件覆盖 | 跨重启保持(写入缓存) |
|
||
|
||
> 插件初始化读配置(如 `[workers.config].database`)请用 `config::effective_config_section`
|
||
> 或 `database::init_database`(内部已按模式解析),**不要**直接读
|
||
> `load_plugin_config(...).runtime["config"]`——后者恒为 toml 种子,cache 模式下会与网关
|
||
> `get_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 支持 sqlite(`local-cache`,本机与 Python 共享同一 .db)+ Redis(`redis-cache`,跨主机);缺省无缓存回退 |
|
||
| 数据库 | 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`) |