【Eclipse OpenSOVD学习之八】 路由与处理器
2026/9/11 19:23:40 网站建设 项目流程

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 三个设计约束

  1. 一致性快照:一次请求内可能多次查拓扑(例如"先确认 component 存在,再取其 apps"),必须落在同一快照,否则会返回不一致的结果。
  2. 能力即存在性:只有当实体确实拥有某能力(如挂了 DataProvider、有 area_id)时才在 capabilities 中给出对应 href。
  3. 统一错误格式:所有错误必须是GenericError,且内部错误细节不能外泄。

2. 当前实现架构

2.1 路由表(opensovd-server/src/routes/

方法路径Handler位置
GET/v1/root_capabilitiesentities/mod.rs:49
GET/v1/componentscomponent_listentities/component.rs:40
GET/v1/components/{id}component_capabilitiesentities/component.rs:75
GET/v1/components/{id}/hostscomponent_hostsentities/component.rs:132
GET/v1/components/{id}/belongs-tocomponent_belongs_toentities/component.rs:173
GET/v1/appsapp_listentities/app.rs:37
GET/v1/apps/{id}app_capabilitiesentities/app.rs:73
GET/v1/apps/{id}/is-located-onapp_is_located_onentities/app.rs:118
GET/v1/apps/{id}/belongs-toapp_belongs_toentities/app.rs:151
GET/v1/areasarea_listentities/area.rs:36
GET/v1/areas/{id}area_capabilitiesentities/area.rs:72
GET/v1/areas/{id}/containsarea_containsentities/area.rs:105
GET/v1/{components,apps}/{id}/data-categories*_data_categoriesdata.rs:67 / 245
GET/v1/{components,apps}/{id}/data-groups*_data_groupsdata.rs:98 / 276
GET/v1/{components,apps}/{id}/data*_data_listdata.rs:160 / 316
GET/v1/{components,apps}/{id}/data/{data_id}*_data_readdata.rs:199 / 355
PUT/v1/{components,apps}/{id}/data/{data_id}*_data_writedata.rs:225 / 381
GET/version-infoversion_infoversion.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: ... })) // ⑤ 封装响应

关键点topoTopologyReadGuard)的生命周期跨越了第 ④ 步的.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 并发与性能(严重)

  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即可解锁此优化。这是全项目收益最高的单点优化

  2. include-schema每次重算schema_for!(中)data.rs:192/348在请求路径上生成 schema,未缓存。schema 是静态的,应 lazy static 缓存。

4.2 语义正确性(中)

  1. groups 与 categories 同时传参被静默吞掉(中):应 400 +incomplete-request,否则客户端误判。
  2. 写请求体无WithRejection(中)PUTJson(body)解析失败时返回 axum 默认纯文本 400,破坏 SOVD 统一错误格式;与读路径的WithRejection<Query<..>>不一致。
  3. DataCategory::Custom("")(低)?category=(空值)会变成Custom("")data-groups静默返回空列表而非 400。
  4. errors字段恒为None(低)ReadResponse.errors从未填充,部分成功/部分失败的语义未实现。

4.3 信息丢失(中)

  1. list端点丢弃每个 item 的 schema(中)include_schema时返回的是响应信封Items<Metadata>的 schema,而每个数据项自己的 schema 被丢弃(映射时未带m.schema)。客户端无法知道每项数据的结构。
  2. is_readable/is_writable不出现在响应中(高,同 03 章):core 的Metadata有这两个标志,models 层没有 →客户端只能靠 PUT 试探才能知道某项是否可写
  3. Response<T>未 deriveJsonSchema(低)include-schema只能给到Items级别,无法描述带schema字段的完整信封。

4.4 能力覆盖(严重)

  1. 22 项能力仅填充约 9 项(高)..Default::default()使faultsoperationsconfigurationsbulk-datadata-listsmodeslockslogsupdatesfunctionssubcomponentssubareascyclic-subscriptions等恒为None。详见 01 章 §4.1。
  2. 无实体写操作(中)POST/PUT/DELETE实体完全缺失,拓扑只能通过代码或发现变更。对于"服务端"定位是合理的(网关聚合场景),但限制了作为独立 server 的可用性。

4.5 建议的改进顺序

优先级事项
P0Box<dyn DataProvider>Arc<dyn ...>,handler 取到 provider 后立即放锁
P0modelsMetadata补齐is_readable/is_writable/schema
P1写请求体加WithRejection,统一错误格式
P1缓存 schema(lazy static)
P1groups+categories 同传 → 400
P2list响应携带每项 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 路由与 handleropensovd-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 handleropensovd-server/src/routes/data.rs:199-243
错误枚举与映射opensovd-server/src/routes/error.rs:20-84
版本信息opensovd-server/src/routes/version.rs:30

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询