07. 路由与处理器
1. 背景与原理
1.1 SOVD 路由的层次
SOVD API 分三层:
/sovd/version-info 版本发现(不在 /v1 下,因为发现必须先于版本化访问) /sovd/v1/ 根能力 /sovd/v1/{collection} 实体集合 + 单个实体能力 /sovd/v1/{collection}/{id}/{relation} 关系(hosts / belongs-to / is-located-on / contains) /sovd/v1/{collection}/{id}/data[/...] 数据资源1.2 三个设计约束
- 一致性快照:一次请求内可能多次查拓扑(例如"先确认 component 存在,再取其 apps"),必须落在同一快照,否则会返回不一致的结果。
- 能力即存在性:只有当实体确实拥有某能力(如挂了 DataProvider、有 area_id)时才在 capabilities 中给出对应 href。
- 统一错误格式:所有错误必须是
GenericError,且内部错误细节不能外泄。
2. 当前实现架构
2.1 路由表(opensovd-server/src/routes/)
| 方法 | 路径 | Handler | 位置 |
|---|---|---|---|
| GET | /v1/ | root_capabilities | entities/mod.rs:49 |
| GET | /v1/components | component_list | entities/component.rs:40 |
| GET | /v1/components/{id} | component_capabilities | entities/component.rs:75 |
| GET | /v1/components/{id}/hosts | component_hosts | entities/component.rs:132 |
| GET | /v1/components/{id}/belongs-to | component_belongs_to | entities/component.rs:173 |
| GET | /v1/apps | app_list | entities/app.rs:37 |
| GET | /v1/apps/{id} | app_capabilities | entities/app.rs:73 |
| GET | /v1/apps/{id}/is-located-on | app_is_located_on | entities/app.rs:118 |
| GET | /v1/apps/{id}/belongs-to | app_belongs_to | entities/app.rs:151 |
| GET | /v1/areas | area_list | entities/area.rs:36 |
| GET | /v1/areas/{id} | area_capabilities | entities/area.rs:72 |
| GET | /v1/areas/{id}/contains | area_contains | entities/area.rs:105 |
| GET | /v1/{components,apps}/{id}/data-categories | *_data_categories | data.rs:67 / 245 |
| GET | /v1/{components,apps}/{id}/data-groups | *_data_groups | data.rs:98 / 276 |
| GET | /v1/{components,apps}/{id}/data | *_data_list | data.rs:160 / 316 |
| GET | /v1/{components,apps}/{id}/data/{data_id} | *_data_read | data.rs:199 / 355 |
| PUT | /v1/{components,apps}/{id}/data/{data_id} | *_data_write | data.rs:225 / 381 |
| GET | /version-info | version_info | version.rs:30 |
共23 条路由。注意:实体路由全是 GET,无增删改——拓扑变更只能通过代码注入或 DiscoveryProvider(见 04 章)。
2.2 应用状态与提取
#[derive(Clone)] pub struct AppState<V> { pub vendor_info: Option<V>, pub topology: Topology } impl<V> FromRef<AppState<V>> for Topology { fn from_ref(state: &AppState<V>) -> Topology { state.topology.clone() } }借助FromRef,handler 可直接写State(topology): State<Topology>,无需接触整个AppState。
3. 核心流程与算法
3.1 Handler 统一骨架(10 个数据 handler 完全同构)
let topo = topology.read().await; // ① 取读锁 let entity = topo.get_component(&component_id) // ② 查实体 .map_err(|_| Error::EntityNotFound(component_id.clone()))?; let provider = entity.data_provider() // ③ 取 provider .ok_or_else(|| Error::ProviderNotAvailable("data".into()))?; let items = provider.categories().await?; // ④ await 业务 Ok(Json(Response { data: ..., schema: ... })) // ⑤ 封装响应关键点:topo(TopologyReadGuard)的生命周期跨越了第 ④ 步的.await——见 §4.1 缺陷 1。
3.2 查询参数解析与过滤映射
SOVD 采用form style、explode=true的重复键语义:?groups=a&groups=b&tags=x。
pub struct DataQuery { pub groups: Option<Vec<String>>, pub categories: Option<Vec<String>>, pub tags: Option<Vec<String>>, #[serde(default, rename = "include-schema")] pub include_schema: bool, }映射算法:
fn data_filter(query: DataQuery) -> DataFilter { let groups = query.groups.unwrap_or_default(); let categories = query.categories.unwrap_or_default(); let scope = if !groups.is_empty() { Some(DataScope::Groups(groups)) } else if !categories.is_empty() { Some(DataScope::Categories(categories)) } else { None }; DataFilter { scope, tags: query.tags.unwrap_or_default() } }groups 优先,categories 被静默丢弃。
提取器使用WithRejection<Query<DataQuery>, Error>,解析失败 →Error::BadQuery→ 400 +incomplete-request。
3.3 百分号编码算法
路径段必须严格按 RFC 3986 编码(实体 ID 允许任意字符串):
constPATH_SEGMENT_ENCODE_SET:&AsciiSet=&CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'?').add(b'`').add(b'{').add(b'}').add(b'/').add(b'%').add(b'&').add(b'+');// 示意,实际以源码为准fnencode_path_segment(s:&str)->String{utf8_percent_encode(s,PATH_SEGMENT_ENCODE_SET).to_string()}自定义AsciiSet而非percent_encoding预设集,是为了精确控制哪些字符需要转义(保留-._~等未保留字符)。
3.4 集合列举与 tags 过滤
let items: Vec<EntityReference> = components .filter(|e| { let tags = e.tags(); query.tags.is_empty() || query.tags.iter().any(|t| tags.contains(t)) // OR 语义 }) .map(|e| EntityReference { id, name, translation_id, href, tags }) .collect();3.5 错误映射算法
| 变体 | HTTP | error_code | vendor_code | | EntityNotFound(id) | 404 | vendor-specific | entity-not-found | | ProviderNotAvailable(p) | 404 | vendor-specific | provider-not-available | | Data(NotFound) | 404 | error-response | — | | Data(ReadOnly) | 400 | error-response | — | | Data(Internal) | 500 | error-response | 消息脱敏 | | BadQuery | 400 | incomplete-request | — | | Topology(NotFound) | 404 | error-response | — |内部错误脱敏:
let message = match e { DataError::Internal(msg) => { tracing::error!(target: "srv", error = %msg, "Internal error"); "An internal error occurred".to_string() } _ => e.to_string(), };3.6 Schema 惰性生成
schema:query.include_schema.then(EntityCapabilities::schema)bool::then(f)—— 只有include_schema=true时才调用(惰性),避免所有请求都付出 schema 生成成本。
4. 待完善与风险
4.1 并发与性能(严重)
读锁跨
await(高):所有 handler 在provider.read/list/write期间一直持有TopologyReadGuard。后果:- 慢 provider(例如转发到 UDS 的请求,可能数十毫秒到秒级)会阻塞所有拓扑写入者(发现事件、运行时变更)
- 锁持有时间 = 整个请求耗时,吞吐受限于"最慢的 provider"
修复建议:先取锁拿到 provider 的克隆、立即放锁、再 await。当前阻碍是
data_provider: Option<Box<dyn DataProvider>>不可 cheap clone(见 03 章 §4.2 缺陷 5)——把Box改为Arc即可解锁此优化。这是全项目收益最高的单点优化。include-schema每次重算schema_for!(中):data.rs:192/348在请求路径上生成 schema,未缓存。schema 是静态的,应 lazy static 缓存。
4.2 语义正确性(中)
- groups 与 categories 同时传参被静默吞掉(中):应 400 +
incomplete-request,否则客户端误判。 - 写请求体无
WithRejection(中):PUT的Json(body)解析失败时返回 axum 默认纯文本 400,破坏 SOVD 统一错误格式;与读路径的WithRejection<Query<..>>不一致。 DataCategory::Custom("")(低):?category=(空值)会变成Custom(""),data-groups静默返回空列表而非 400。errors字段恒为None(低):ReadResponse.errors从未填充,部分成功/部分失败的语义未实现。
4.3 信息丢失(中)
list端点丢弃每个 item 的 schema(中):include_schema时返回的是响应信封Items<Metadata>的 schema,而每个数据项自己的 schema 被丢弃(映射时未带m.schema)。客户端无法知道每项数据的结构。is_readable/is_writable不出现在响应中(高,同 03 章):core 的Metadata有这两个标志,models 层没有 →客户端只能靠 PUT 试探才能知道某项是否可写。Response<T>未 deriveJsonSchema(低):include-schema只能给到Items级别,无法描述带schema字段的完整信封。
4.4 能力覆盖(严重)
- 22 项能力仅填充约 9 项(高):
..Default::default()使faults、operations、configurations、bulk-data、data-lists、modes、locks、logs、updates、functions、subcomponents、subareas、cyclic-subscriptions等恒为None。详见 01 章 §4.1。 - 无实体写操作(中):
POST/PUT/DELETE实体完全缺失,拓扑只能通过代码或发现变更。对于"服务端"定位是合理的(网关聚合场景),但限制了作为独立 server 的可用性。
4.5 建议的改进顺序
| 优先级 | 事项 |
|---|---|
| P0 | Box<dyn DataProvider>→Arc<dyn ...>,handler 取到 provider 后立即放锁 |
| P0 | modelsMetadata补齐is_readable/is_writable/schema |
| P1 | 写请求体加WithRejection,统一错误格式 |
| P1 | 缓存 schema(lazy static) |
| P1 | groups+categories 同传 → 400 |
| P2 | list响应携带每项 schema |
| P2 | 按能力优先级逐步填充 capabilities(faults → operations → modes → locks) |
5. 关键代码位置
| 内容 | 路径 |
|---|---|
| 路由组装 | opensovd-server/src/routes/mod.rs:66-85 |
| 根能力 | opensovd-server/src/routes/entities/mod.rs:49-72 |
| 百分号编码集 | opensovd-server/src/routes/entities/mod.rs:79-99 |
| component 路由与 handler | opensovd-server/src/routes/entities/component.rs:27-... |
| app / area handler | .../app.rs:27-...、.../area.rs:27-... |
| 数据路由注册 | opensovd-server/src/routes/data.rs:37-62 |
data_filter映射 | opensovd-server/src/routes/data.rs:138-154 |
| read / write handler | opensovd-server/src/routes/data.rs:199-243 |
| 错误枚举与映射 | opensovd-server/src/routes/error.rs:20-84 |
| 版本信息 | opensovd-server/src/routes/version.rs:30 |