2026-08-31 16:34:13 +08:00
|
|
|
|
# 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]`
|
|
|
|
|
|
|
2026-09-04 14:50:34 +08:00
|
|
|
|
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)。
|
2026-08-31 16:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
编译启用:`bash build.sh dist "redis-cache"`(或 Cargo 依赖加 `chongming-worker-rs/redis-cache`)。
|
|
|
|
|
|
未配置时单实例直接读写本地 `config.toml`(默认无缓存),行为一致。
|
|
|
|
|
|
|
2026-09-17 15:33:35 +08:00
|
|
|
|
#### 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")。
|
|
|
|
|
|
|
2026-08-31 16:34:13 +08:00
|
|
|
|
### 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 一致 → 网关幂等去重零冲突,负载均衡透明分发。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-10-13 17:29:38 +08:00
|
|
|
|
## 9. 本地联调
|
2026-08-31 16:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-10-13 17:29:38 +08:00
|
|
|
|
# 起一个本地 Zenoh Router(替代 zenohd)
|
2026-08-31 16:34:13 +08:00
|
|
|
|
.venv/bin/python utils/worker-rs/scripts/zenoh_router.py &
|
|
|
|
|
|
|
2026-10-13 17:29:38 +08:00
|
|
|
|
# 协议级端到端自检(描述符 / RPC / 流式 / 错误 / 摘除 / 自愈)
|
2026-08-31 16:34:13 +08:00
|
|
|
|
.venv/bin/python utils/worker-rs/scripts/verify_e2e.py \
|
|
|
|
|
|
--type myplugin --bin workers/myplugin/target/debug/myplugin
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-10-13 17:29:38 +08:00
|
|
|
|
## 10. Rust 侧行为要点
|
2026-08-31 16:34:13 +08:00
|
|
|
|
|
2026-10-13 17:29:38 +08:00
|
|
|
|
| 要点 | 说明 |
|
2026-08-31 16:34:13 +08:00
|
|
|
|
| ---- | ---- |
|
2026-10-13 17:29:38 +08:00
|
|
|
|
| 契约校验 | **发布前自检**:无 handler 的端点告警跳过,不发布残缺路由 |
|
|
|
|
|
|
| 模型生成 | `gen_models` 生成强类型结构体 + **启动时指纹校验**(TOML 变更未重生成即告警) |
|
2026-08-31 16:34:13 +08:00
|
|
|
|
| handler 注入 | worker_id 经 `Request.worker_id`、user_info 经 `Request.user_info` 注入(非函数参数) |
|
2026-10-13 17:29:38 +08:00
|
|
|
|
| 配置缓存后端 | `local-cache`(sqlite,与 Python 共享同一 .db)/ `redis-cache`(跨主机);缺省无缓存回退 |
|
|
|
|
|
|
| 数据库 | 走 sqlx(见 §7),配置键与 `chongming_database` 对齐 |
|
2026-08-31 16:34:13 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 参考材料
|
|
|
|
|
|
|
|
|
|
|
|
- 线协议规范:`./plugin-wire-protocol.md`(唯一契约,[附录 A 事实来源索引](./plugin-wire-protocol.md))
|
|
|
|
|
|
- 多语言演进规划:`./multi-language-plugin-plan.md`(含数据库/缓存对齐表)
|
|
|
|
|
|
- 完整示例插件:`../workers/rsdemo/`(流式、错误、数据库、管理指令全覆盖)
|
|
|
|
|
|
- 脚手架:`uv run gen_plugin --help`(`--rust`)
|