ek-core 0.4.3

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 验收记录(TESTPLAN)


版本 0.3.0。运行方式:`cargo test`(依赖 crates.io 的 ek-event 0.1.0)。
最终状态:**45 用例全绿(矩阵 44 + lib 单元 1[lifecycle 键形]),3× 重复 0 失败;`cargo clippy --all-targets -- -D warnings` EXIT 0;全套耗时 ~4s。**

## 1. 用例矩阵与结果


### service_disciplinary(7,v0.2.0 补强)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| SD1 memoize 失效+扩容 | 可变目录扩容(eExpand → 先换 services() 再 publish)→ 索引双条 → services_of len=2(世代失效不可能陈旧);扩容前 alias=0 | ✅ |
| SD2 清空↔复活 | publish 空 → 全 id 消失 + ask → UnknownService;publish 恢复 → find 复活(双向) | ✅ |
| SD3 双键载荷 | catalog 改版固定键 `eSrvChg` 载荷=PlugNotice / 后缀键 `eSrvChg.<名>` 载荷=版本串,两群订阅各自命中 | ✅ |
| SD4 跨熔断失效转移 | p1 连 3 panic 熔断;ask_service 第 1-3 问 TargetGone、第 4 问由宿主自动选 Running 的 p2 → Ok(200+n);stats.panics==3 | ✅ |
| SD5 未知面板 | services_of(ghost)=None / find 空 / plugins() 无 ghost / get_state/get_plugin/mail_stats 全 None | ✅ |
| SD6 空版本语义 | 默认 version()="" → ePlugUp 载荷空版本;PluginInfo.version=="阵"空;注意:watch 自身 launch 的 ePlugUp 也入记录(单播自发) | ✅ |
| SD7 首 Running+内部重选 | 双 Running 提供方 ask → 首(t1)出品;remove 后不靠 ePlugDown 下一次 ask 自动 p2 出品 | ✅ |

### dynamic_lifecycle(7,v0.2.0 补强)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| DL1 迟到车道讲话 | 动态 add 同步装配 → 100 条両订阅者全数到齐(迟装不漏) | ✅ |
| DL2 换血 | remove → 同名新 Arc add:旧实例计数冻结、ePlugUp 共 2 发、旧 on_stop 已调、新 Running | ✅ |
| DL3 重名拒机 | 运行期重名 Duplicate,原件照干(5 计数) | ✅ |
| DL4 remove 错名 | UnknownName;close 后 → Stopped | ✅ |
| DL5 Ready remove | start 前 remove → Ok、on_stop 零调用(D4 判据) | ✅ |
| DL6 忙者移除 | 在途 ask 处理中(占用 600ms)→ remove → 排空交付 ok:7;**移除后索引已剪 → 后续 ask_service 得 UnknownService(F3:索引二十大真相的表现,QA 本次 URL:此前「假设 TargetGone」按真語义收敛)** | ✅ |
| DL7 多动态 + 反序 | 两个动态插件 close 反序 late2 → late1 → first | ✅ |

### strict_contracts(3,v0.2.0 补强)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| SC1 ask 自呼 | 自己等自己 → 满超时 Timeout(非死锁非 panic);0 panic;随后 normal ask → Ok;协议违约不伤框架 | ✅ |
| SC2 notify 未知 | 静默 g3(队列排空为零)、同伴订阅者照干、0 panic | ✅ |
| SC3 eTimer 穿透 | ek-event 1s 心跳→ forwarder→sink插件 mailbox→on_mail | ✅ |

### dynamic_storm(1,v0.2.0 补强)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| DS1 动态成员风暴 | 5000 双阶段限速:静态 c1 全收 5000;动态 c2 恰收 phase-B 2000(add 后不漏不错);零 spilled;风暴中途 remove c1 → ePlugDown ×1 + 后置 200 全由 c2 承接(2200 精确)+c1 冻结 + 全程零死锁 | ✅ |

### discovery(7)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| DV1 发现命中 | `find_service` 命中(名字/版本/doc/req_type=type_name 现场一致);`services_of(broker)` 完整目录;无人提供的服务 → `find_service` 空 + ask_service → `UnknownService`(<100ms) | ✅ |
| DV2 未命中降级 | 无提供方时插件在 `on_init` 自查并**自持降级旗标**(0.3.0:宿主零日志,无 write_log;降级是插件自己的状态) | ✅ |
| DV3 ePlugUp 迟绑定 | start 时无提供方(init 断言空)→ **运行期动态 add** broker → 即刻 `Running` + `ePlugUp` 广播(固定键载荷 PlugNotice)→ A 自主重配(记录 name/2.0.0) | ✅ |
| DV4 目录改版 | DynProvider 先换 services() 再 `publish_services` → 索引重织 + `eSrvChg` 广播 → A 重查:旧 id 0 家(幽灵被除)、新 id 1 家,旧 id ask → `UnknownService` | ✅(教训:先换目录再申报;已固化为 plugin.rs/context.rs 纪律) |
| DV5 failover+版本迁移 | 双提供方(1.0.0/1.1.0)按注册序配对前者;`remove_plugin` 旧方 → `ePlugDown` → A 被动迁移接 `broker_new/1.1.0`;索引同步修剪 | ✅ |
| DV6 ask_service 三闸门 | ①UnknownService(索引空)②InvalidPayload(Req 型别与声明不符,**发出前拦截**,不烧一次往返)③契约匹配 Ok(Pong{n})原样返回 | ✅ |
| DV7 动态生命周期 | start 后 add → 即刻 Running + `started_order` 覆盖(close 反序:动态插件先关);close 后 add → `Stopped` | ✅ |

### host_lifecycle(5)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| HL1 注册校验 | 空/数字开头/空白/非 ASCII/33 字符 → `IllegalName`;重名 → `Duplicate`;**新 D1**:add 期悬空 deps 放行、start 期才报 `UnknownDep`;自依赖 add 期即 `DepCycle`;信息快照 Ready(v0.2.0 起 HostStarted 断言废止,动态 add 见 DV3/DV7) | ✅ |
| HL2 拓扑序/逆序 | B→A→C 依赖链:start 记录 b,a,c;close 记录 c,a,b;close 后表空、状态 None | ✅ |
| HL3 关停静默 | close 后总线上 put 不再进插件 | ✅ |
| HL4 不回放 | start 前流出事件不回放(对齐 g3);start 后事件逐速送达 | ✅(依赖 ek-event start() 幂等,receiver-take 守卫) |
| HL5 信息/统计 | get_state / mail_stats 可查 | ✅ |

### actor_routing(2)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| AR1 FIFO+worker 线程探针 | 200 事件保序;on_mail 全部发生在 `ek-worker.oms`(分发线程零业务) | ✅ |
| AR2 订阅面隔离 | pa/pb 各 3 条,互不串门 | ✅ |

### ask_oneshot(5)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| AO1 并发配对 | 4 个 asker worker 并行 ask,100 次按 id 精确配对;broker 精确处理 100 | ✅ |
| AO2 超时 | 目标处理 600ms > timeout 200ms → `Err(Timeout)`,耗时贴近 | ✅ |
| AO3 错型 | 载荷契约不匹配 → B 侧回 `InvalidPayload`,A 侧类型自查 | ✅ |
| AO4 目标不在 | 预检立即 `TargetGone`(<100ms,不烧超时) | ✅ |
| AO5 panic 断连 | 目标 handler panic → 信封 drop → 快速 `TargetGone`(<400ms);单次 panic 不熔断(目标仍 Running) | ✅ |

### fault_isolation(6)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| FI1 熔断检疫 | 连 3 panic → `Quarantined`;`ePlugQDown.p3` ×1(0.3.0:唯一结构化通道,无 eLog);邻居完好;检疫后不再收邮件 | ✅ |
| FI2 慢邻居 | 80ms/事件的慢 worker 不拖累其他插件(fast 应答完成时 slow 仍在处理第 1 件) | ✅ |
| FI3 背压 | cap=32 + 200 突发:processed + spilled == 200 守恒;溢出为多数 | ✅ |
| FI4 关停超时 | 忙 worker(3s)下 close(200ms) 快速返回<1.5s;剥离后稍后排空自退 | ✅ |
| FI5 熔断后 on_stop(F1 修正) | Quarantined 插件 close 仍调 on_stop,Ready 不调 | ✅ |
| FI6 连续语义(F3 修正) | 3 次被成功处理隔开的 panic 不熔断;连 3 panic 即熔断;累计统计面=6 | ✅ |

### concurrency_storm(1)


| 用例 | 断言要点 | 结果 |
| --- | --- | --- |
| CS1 全路径风暴 | 2 生产线程 ×5k(限速 25/2ms)+ 80 并发 ask(4 asker)+ 50 notify + 200 轮 get_state/mail_stats 轮询;终态:两订阅者各 10000、零溢出(spilled==0)、80 ask 精确配对、全部插件 Running、零死锁 | ✅ |

## 2. 方法纪律


- 零业务依赖:全部测试用测试本地结构体(与 ek-event 生态用例同一惯例),不 import 任何业务 crate。
- `wait_until(轮询)` 助手,绝对不使用 `sleep(断言)`;时间裕量仅在"消除竞态"的文档型前提(HL4 的 200ms)首次出现。
- 真断言错误路径用 `matches!` 精确匹配变体;数值路径用 id 配对。稳定性以 3× 重复运行验证。

## 3. 失败记录 → 修复映射(执行全程)


| 阶段 | 记录 | 修复 |
| --- | --- | --- |
| 二轮方案审核 | #1 forwarder_ids 缺失 / #2 TargetGone 宣称过强 / #3 ctx.engine 访问器矛盾 / #4 eReq 溢出语义未定 / #5 remove 策略未定 / #6 forwarder 短路 / #7 头阻塞 | 全部落实现 + 文档 + 3 项由用户拍板(允许 remove / cap 覆写 / 头阻塞接受) |
| 编译期 | Event 无 Clone(R3)→ forwarder 手工复制/envelope Ok/Err 泛型推断/OneshotSender &self(R2)/registry indeg 索引/get_typed Arc clone | 全部按编 error 修复 |
| 执行期 | R1 信封双层泛型错型不可达 → 非泛型信封抹平;R4 CS1 守恒断言与 D5 冲突 → 限速+spilled==0 | 见 DESIGN §3 |
| 执行后复审 | F1 on_stop 判据(Quarantined/Exited 也调);F2 禁自呼文档;F3 连续 panic 语义;F4 eReq 溢出文档口径;F5 注释乱码 | 全部已修并新增 FI-5/FI-6 用例锁语义 |
| 0.2.0 方案审核 | F1 add 期 UnknownDep 与注册自由矛盾;F2 HostStarted 与动态 add 冲突;F3 remove 后索引幽灵;F4 缓存无失效规则;F5 PluginSummary 与 PluginInfo 重复;F6 version() 默认宏填错 crate | F1/F2 按新 D1 改 wiring;F3 remove 修剪;F4 世代计数(revision;`gen` 为 Rust 2024 保留字);F5 复用 PluginInfo+version;F6 默认空串+文档警告;新增 DV1–DV7 锁语义 |
| 0.2.0 执行期 | DV4 首版失败:publish 只改索引、插件 services() 未换内容 → live 细节与索引脱节(peg=0) | 固化为纪律「先换 services() 再 publish_services」(plugin.rs/context.rs 文档 + DynProvider 测试件模拟真实可变目录) |
| 0.2.0 充分测试期 | DL6 揭示 remove 后 ask_service = UnknownService(非 TargetGone —— 索引已剪即 no provider);SD6 揭示 ePlugUp 含「自身 launch 的自发」;DS1 发现相对基线的 ±1 竞态 → 改精确总量断言;SC1 排障中自查出丢失 wait_until(方法纪律违背,即刻纠正) | 全部按真语义收敛用例 + 用例头注明语义出处;probe_min 临时件用后即删 |

## 4. 基准与局限


- 待办(未来测评):forwarder `Event` 手工复制在百万级/秒下的 String 重分配压力;若成为瓶颈,启用 ek-event 0.2 候选项:`event_type` 改 `Arc<str>`,或原生「订阅直达邮箱」API。
- 已知接受的取舍见 DESIGN §5;暂不支持运行期动态订阅(ek-event 精确匹配;lifecycle 广播用「固定键 + 后缀键」双发缓解)。
- 极端并发下 `close + remove` 双调窗口:宿主管理操作按串行使用约定(文档已写)。
- 运行期动态 add/remove 已支持(v0.2.0);**不支持**运行期替换 crate 代码(进程级部署,任何 Rust 框架同理);同注册名双版本真并存(`name@version` 路由)列为 0.3 路线项。