# ek-core 设计文档(DESIGN)
Actor-over-Bus 插件宿主,基于 [ek-event 0.1.0](https://crates.io/crates/ek-event)。
本文档描述 v0.1.0 的结构与全部语义决议;与其同步的验收记录见 [TESTPLAN.md](TESTPLAN.md)。
## 1. 体系定位
```
┌────────────────────────────────────────────────────────────┐
│ 业务 crate(未来):数据对象 + 业务事件常量 + 插件实现+ 消息契约 │
├────────────────────────────────────────────────────────────┤
│ ek-core(本 crate):纯宿主,零业务知识 │
│ Plugin trait / MainEngine / registry / mailbox+worker │
│ Envelope+Oneshot / forwarder / 熔断检疫 / 内置 LogPlugin │
├────────────────────────────────────────────────────────────┤
│ ek-event 0.1.0(crates.io):无界队列 + 单分发线程 + 定时器 │
└────────────────────────────────────────────────────────────┘
```
**铁律**:ek-core 只依赖 ek-event;所有跨插件通信都走事件;总线只转发,永不执行业务;宿主不做任何业务路由(依赖由 `deps()` 显式声明)。
## 2. 锁定决议(D1–D6)
| D1 | 插件间**禁止直调**;通信 = 广播 / `eReq.<target>` 请求 / `eTo.<target>` 通知;依赖 `deps()` 显式声明,注册期校验、start 期拓扑 |
| D2 | 插件名 `[a-zA-Z][a-zA-Z0-9_-]{0,31}`(首字符字母);注册键唯一 |
| D3 | handler **连续** panic×3 → `Quarantined` + `ePlugQDown.<name>` + eLog(双告警);处理成功即复位连续计数 |
| D4 | close 五步:卸 forwarder → `on_stop`(逆依赖序,曾装配即调)→ 关邮箱 → 排空 → join(超时告警剥离) |
| D5 | 邮箱 crossbeam bounded(默认 1024,`mailbox_cap()` 覆写)+ `try_send` 丢弃计数;Oneshot = std::sync::mpsc + AtomicBool 单发守卫 |
| D6 | 内置 `LogPlugin`(仅订阅 `eLog`),宿主级通用日志,非业务 |
## 3. 模块与数据结构
| `plugin.rs` | ~46 | `Plugin` trait(name/deps/routes/mailbox_cap/on_init/on_start/on_stop/on_mail) |
| `registry.rs` | ~130 | `PluginEntry{plugin, any, state, mailbox, forwarder_ids, worker, stats}`;状态 CAS `PluginStateCell`;Kahn 拓扑 |
| `main_engine.rs` | ~285 | 宿主:统一注册校验/typed get/start 逆序 close 五步/单插件 remove(允许+警告)/统计 |
| `forwarder.rs` | ~74 | 只转发闭包(+手 copy Event——ek-event 0.1.0 无 Clone;负荷零拷贝):alive 短路 + 溢出节流 |
| `worker.rs` | ~86 | actor 循环:recv → catch_unwind(on_mail) → 连续 panic 熔断 |
| `envelope.rs` | ~110 | 非泛型信封 `{payload: Arc<dyn Any>, reply: OneshotSender<Box<dyn Any>>}`;B 侧通用 `handle` |
| `context.rs` | ~86 | `PluginContext`:put / ask(预检→put→recv_timeout→downcast) / notify / write_log;无 engine 访问器 |
| `log_plugin.rs` / `error.rs` / `lib.rs` | ~110 | 默认日志 sink / 全部错误面 / re-export |
关键技术点:
- **双 trait 对象同槽**:`PluginEntry` 持 `Arc<dyn Plugin>`(调用)+ `Arc<dyn Any+Send+Sync>`(同配,downcast 用);`get_typed::<T>` 即一次 `Arc::downcast`。
- **邮箱不承载控制消息**:关/挂/排空全用 "全部 Sender 消费尽→跨 beam 断连" 一个原语表达(D4 第③④步零额外协议)。
- **请求契约抹平**(R1 动修):双层泛型 `<Req,Res>` 信封在错型时拿不到回包 Sender、且 Box 整封 downcast 有 TypeId 混淆 → 非泛型信封,payload 由 B downcast(错型→`Err(InvalidPayload)`),回包由 A downcast(错配同样落 `InvalidPayload`)。
- **Oneshot &self**(R2 动修):worker 收的是 `&Event`,无法从信封 move 出 Sender → `AtomicBool sent` 守卫,只让第一次 `send` 生效。
- **panic 断连信令**:事件变量在 worker 循环持有,on_mail panic 被 catch 截断后 `event` 随轮作用域 drop → 信封 reply drop → 信道关闭 → 发起方收 `TargetGone`(勿改为引用投递)。
## 4. 生命周期
```
Ready ──start()──▶ Running ──close()──▶ Stopping ──worker exit──▶ Exited
│
└── 连续 panic×3 ──▶ Quarantined(worker 自退)
```
- start:`on_init` → 建邮箱 → spawn worker → state=**Running** → 注册 forwarder → `on_start`(先 Running 后装车道,避免 ask 预检窗口误判)。
- close(对每个插件,逆 start 序):①unregister forwarder → ②on_stop → ③宿主 Sender drop → ④排空 → ⑤join(timeout,超时告警 + 剥离)。
- remove(单插件):Running 者五步;Exited/Quarantined 摘遗留 forwarder;仍被 deps 引用 → eLog 警告后仍允许移除(定稿 policy)。
- close 把插件表清空;`close_all` 额外停总线;不可复活(stop 不可逆,对齐 ek-event)。
## 5. 错误面与复述
- `PluginError::{IllegalName, Duplicate, UnknownDep, DepCycle, UnknownName, HostStarted, Stopped}`
- `AskError::{Timeout, TargetGone, InvalidPayload, BusStopped}` — TargetGone 是「尽力而为」:预检 + 断连快失败,极端 TOCTOU 窗口由 Timeout 兜底。
- 已知取舍:selected:头阻塞(broadcast 与 eReq 混排)接受于订阅面分离(建议 req-only 插件 или拆两个插件),双通道 select 列后续选型;管理操作串行假设;on_stop 钩子由插件作者保证不阻塞。
## 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。