ek-core 0.5.2

Business-agnostic plugin host and actor-over-bus runtime on top of ek-event: capability index with runtime service discovery, per-plugin actor workers, dynamic add/remove with lifecycle broadcasts, one-shot ask/ask_service with contract gates, panic quarantine, five-step close.
Documentation
# ek-core 设计文档(DESIGN)


Actor-over-Bus 插件宿主,基于 [ek-event 0.1.0](https://crates.io/crates/ek-event)。
本文档描述 v0.4.0 的结构与全部语义决议(0.4.0 起 含 §7 配置契约);与其同步的验收记录见 [TESTPLAN.md](TESTPLAN.md)。

## 1. 体系定位


```
┌────────────────────────────────────────────────────────────┐
│ 业务 crate(未来):数据对象 + 业务事件常量 + 插件实现 + 消息契约 │
├────────────────────────────────────────────────────────────┤
│ ek-core(本 crate):纯宿主,零业务知识                        │
│   Plugin trait / MainEngine / registry+能力索引 / mailbox    │
│   Envelope+Oneshot / forwarder / 熔断检疫 / lifecycle 常量   │
├────────────────────────────────────────────────────────────┤
│ ek-event 0.1.0(crates.io):无界队列 + 单分发线程 + 定时器     │
└────────────────────────────────────────────────────────────┘
```

**铁律**:ek-core 只依赖 ek-event;所有跨插件通信都走事件;总线只转发,永不执行业务;宿主零业务方法 —— 宿主的全部职责是「登记簿 + 能力索引 + 上下线吭声」。

## 2. 锁定决议(D1–D6)


| 决议 | 内容 |
| --- | --- |
| D1(v0.2.0 修订) | 插件间**禁止直调**;通信 = 广播 / `eReq.<target>` 请求 / `eTo.<target>` 通知。**注册自由**:add 期只报名 + 自报服务目录,互不要求在场;`deps()` 降级为**可选排序提示**(不声明=按注册序),悬空 deps 由 start 期把关。配对、验证、迟绑定皆由插件在运行期自主完成(ctx 四问 + 生命周期广播);v0.1.0 的“注册期 deps 存在性校验 + start 后拒绝 add(HostStarted)”随之废止 |
| D2 | 插件名 `[a-zA-Z][a-zA-Z0-9_-]{0,31}`(首字符字母);注册键唯一(0.2 保留;同注册名双实例真并存 = `name@version` 一等公民,列为 0.3 路线) |
| D3 | handler **连续** panic×3 → `Quarantined` + `ePlugQDown.<name>` 结构化广播(0.3.0:单一通道;诊断文本走 stderr);处理成功即复位连续计数 |
| D4 | close 五步:卸 forwarder → `on_stop`(逆依赖序,曾装配即调)→ 关邮箱 → 排空 → join(超时告警剥离) |
| D5 | **0.5.0 改案**:邮箱 crossbeam **unbounded**(默认**不丢**)+ 深度计数(`pending`/`peak` 实时)+ **软顶**裁决(`mailbox_soft_cap()`,缺省 262144:触顶才丢新帧 + `spilled` 计数);Oneshot = std::sync::mpsc + AtomicBool 单发守卫。(0.4 旧案:bounded 1024 + `try_send` 满即丢 —— 现场实跑里把界面 ask 信封丢掉导致刷新停摆,已废止) |
| D6 | (0.3.0 起废止)内置 `LogPlugin` 已删:宿主**零日志** —— 想说的话全是结构化事件(lifecycle 模块),运维文本只走 stderr,不进总线不占类型 |

## 3. 服务发现与动态生命周期(v0.2.0 核心)


### 3.1 三分法:信息只有一个居所


| 信息 | 居所 | 何时取 |
| --- | --- | --- |
| `svc_id → 提供方名` 纯路由索引 | 宿主(add 期自 `services()` 抽 id 织入) | find_service / ask_service O(1) 命中 |
| 每项服务的完整明细(id/req/res/doc/超时提示) | **插件本身**(宿主不复制登记) | 查询时宿主**现场**调 `services()`(memoize 仅性能缓存,按世代核对) |
| 参数/返回值真约束 | 业务 crate 的契约类型 + rustdoc | 编译期全检;运行期闸门只做 type_name 兜底比对 |

### 3.2 ctx 四问 + 改版


- `plugins() -> Vec<PluginInfo>`(含 version;`PluginInfo` 单一披露面,不再另设 PluginSummary)
- `services_of(name) -> Option<Arc<Vec<ServiceDesc>>>`(Ready 可查:声明在注册时即静态可用)
- `find_service(svc) -> Vec<ProviderRef{name, version, state, ServiceDesc}>`(注册序有序;含非 Running —— 条目=能力声明,可用性交给 ask 闸门)
- `ask_service::<Req,Res>(svc, req, timeout)`:UnknownService(无人提供)/ TargetGone(有声明但全非 Running)/ InvalidPayload(req_type 不符,发出前拦)→ 路由到注册序第一个 Running
- `publish_services(Vec<ServiceDesc>)`:目录改版(宿主重织索引 + 缓存失效 + 双发 `eSrvChg.<名>` / `eSrvChg`)。**铁律:先换 services() 当前返回,再申报**(DV4 教训,现场取原则)

### 3.3 生命周期广播(宿主的全部“吭声”)


| 事件 | 触发 | 载荷 |
| --- | --- | --- |
| `ePlugUp`(固定)/ `ePlugUp.<名>`(后缀) | launch_one 完成(start 与动态 add 共用) | `PlugNotice{name,version}` / 版本串 |
| `ePlugDown` / `ePlugDown.<名>` | remove_plugin(计划内拆单件) | 同上;close() 不发(拆除期零风暴,F7) |
| `ePlugQDown.<名>` | 熔断(v0.1.0,不变;0.3.0 修正:载荷 = 无载荷 unit,源码 `Event::new`) | — |
| `eSrvChg` / `eSrvChg.<名>` | publish_services 目录改版 | `PlugNotice` / 版本串 |

### 3.4 版本与迁移策略


- `Plugin::version()` 默认空 = 「未知版本」;约定 `Cow::Borrowed(env!("CARGO_PKG_VERSION"))`(身份证别在插件 crate 处展开,默认实现会错填 ek-core 的版本 —— F6 已文档化)。
- 消费端策略插件自定;推荐的默认 = **被动跟随**:配对者存活不换轨,`ePlugDown/ePlugQDown` 后自动接注册序下一个 Running(自然可能是新版,即“被迫升级”);诊断留痕走插件自己的通道(0.3.0:宿主零日志)——`prefer_latest`(主动跟新版)由发起方自行决策 —— 宿主只供事实,不代做选择。
- 版本元数据零路由代价;多提供方同 svc_id 天然支持主备/失败转移(failover,注册序优先)。

## 4. 生命周期


```
Ready ──start()/动态add──▶ Running ──close()──▶ Stopping ──worker exit──▶ Exited
                              │
                              └── 连续 panic×3 ──▶ Quarantined(worker 自退)
```

- 装配 = `launch_one`(0.2.0 起 start 与动态 add 共用):`on_init` → 建邮箱 → spawn worker → state=**Running** → 注册 forwarder → `on_start` → 广播 ePlugUp。动态 add 成功后名字记入 `started_order`,close 反序自然覆盖。
- close(逆序):①unregister forwarder → ②on_stop → ③宿主 Sender drop → ④排空 → ⑤join(超时告警 + 剥离);随后清插件表(能力索引/缓存一并清)。close 后一切变更 `Stopped`;不再发逐插件广播。
- remove(单插件):Running 者五步;Ready/Exited/Quarantined 摘遗留 forwarder;**同步修剪能力索引**(防幽灵提供方,F3)+ 失效缓存 + ePlugDown 广播;仍被 deps 引用 → stderr 警告后仍允许移除(0.3.0:不再进总线)。

## 5. 错误面与复述


- `PluginError::{IllegalName, Duplicate, UnknownDep(start 期), DepCycle, UnknownName, Stopped}`(v0.2.0 移除 `HostStarted`)
- `AskError::{Timeout, TargetGone, InvalidPayload, UnknownService(新增), BusStopped}` — TargetGone 是「尽力而为」:预检 + 断连快失败,极端 TOCTOU 窗口由 Timeout 兜底。
- 已知取舍:selected:头阻塞(broadcast 与 eReq 混排)接受于 v0.1;管理操作串行假设;on_stop 钩子由插件作者保证不阻塞;`services()` 纯/静态/无锁纪律(F8)。

## 6. 与 ek-event 的边界


- ek-event 0.1.0 无此改动;forwarder = `register(type, 闭包)` 即用,零 API 变更。
- ek-event `Event` 无 Clone(R3):forwarder 手工复制(仅重建 type String;载荷 Arc 零拷贝)。若未来测得 String 重分配是主要瓶颈,ek-event 0.2 候选项:event_type 改 `Arc<str>`,或原生「订阅直达邮箱」API。

## 7. 配置契约(0.4.0;BD-CFG)


`section` 模块 + `Plugin::as_configurable`,**additive**:存量插件缺省「无配置面」零负担。设计归属三条决议(操作步骤、测试钉定式单源在 [PLUGIN-GUIDE.md](PLUGIN-GUIDE.md);全部钉判据单源在 ek-station `DESIGN.md` §BD-CFG,此处不复制清单):

- **契约单份住本 crate(BD-CFG-2)**:`SectionMap`/`Field`/`Configurable`/`ApplyReport` 全链一份类型。全链(ek-cfg 库、cfg 收件插件、两宿主、任意插件 crate)对配置交换只用这一组类型 —— `ek-cfg` 的 `SaveReq.map` 直接复用 `SectionMap`,零转影零同名异型。
- **纯契约,零格式代码(BD-CFG-3)**:行式键值解析/序列化、文件 IO、原子三步写、秘库(`secret::set/get/...`)全部在 ek-cfg;本 crate 保持纯数据 + trait,零 IO 依赖,与「宿主只管登记簿+吭声」同构。
- **可选能力模式(与宿主零日志同式张力)**:`as_configurable` 缺省 `None`,宿主探查一行事、视而不见 —— 不给简单插件加任何运行时代价;`Some(self)` 是**同一对象借出的第二张脸**(`&dyn Configurable`,调用期借完即还),宿主只留存 ns 字符串进配置目录,不另造注册面。

语义要点(详细口径在以上两个单源,此处只记宿主侧分工):

| 面 | 归属 | 语义 |
| --- | --- | --- |
| `config_ns()` | 插件 | 全链唯一,`add_plugin` 期撞名校验;过 `valid_config_ns` |
| `apply_config(&SectionMap)` | 插件实现,宿主直调 | boot / svctl.start 两路在 `on_start` 前**管理线程**直调;只登记引用,秘面禁取值(BD-CFG-16);`Err` 上抛 = 装配单一硬报错点;`deferred` 非空 = NextStart 档显形 |
| `dump_config()` | 插件 | 受理成功后插件自走三行序(ek-cfg `secret::set` → dump → `cfg.save`);只输出要持久的字段(不输出 = 不持久,BD-CFG-15) |

## 8. 版本历史要点


- **0.4.0**:配置托底契约(`section` 模块 + `Plugin::as_configurable`,additive;§7)—— 契约单份(BD-CFG-2)、零格式代码(BD-CFG-3)、可选能力缺省零负担;解析/IO/秘库全在 ek-cfg(0.1.0/0.2.0),钉清单单源 ek-station DESIGN。

- **0.3.0**:宿主**零日志** —— 删 `LogData`/`LogPlugin`/`EVENT_LOG`/`LOG_LEVEL_*` 与 `ctx.write_log`;宿主想说的话全是结构化事件,四族常量拆至独立 `lifecycle` 模块(`ePlugUp`/`ePlugDown`/`ePlugQDown.<名>`/`eSrvChg`);运维诊断(close 超时/panic/邮箱溢出)只走 stderr,不进总线不占类型;宿主依赖面不变(仅 ek-event)。
- **0.2.0**:服务发现四问 + 能力索引(minimal-knowledge)、ask_service 类型闸门、publish_services 目录改版、动态生命周期(add/remove + ePlugUp/ePlugDown/eSrvChg)、版本元数据、D1 修订。
- **0.1.0**:单表注册 / 拓扑 start / 五步 close / 熔断检疫 / 一次性请求应答 / 内置 LogPlugin(0.3.0 删)。