# ek-core
**Actor-over-Bus 插件宿主**。在已发布的 [ek-event](https://crates.io/crates/ek-event) 事件底座之上,提供一套业务无关的插件框架:自由注册、能力发现、独立线程 actor、一次性请求应答、动态生命周期、故障熔断与五步关停。
> 设计立场:ek-core 不依赖任何业务;事件总线只转发、永不执行业务逻辑;插件之间不直调,一切通信走事件。宿主只有两个职责:**登记簿 + 上下线吭声**。
## 特性
- **注册自由(v0.2.0)**:`add_plugin` 只报名 + 自报服务目录,插件之间在注册期互不认识、任意顺序;`deps()` 仅作**可选排序提示**(不声明=按注册序),悬空依赖由 `start()` 把关。
- **服务发现(0.2.0)**:插件自报 `services()` 目录(服务 id / req-res 类型名 / doc / 超时提示);宿主只织入 `svc_id → 提供方` **纯路由索引**,完整细节由 `ctx.services_of / find_service` **现场**取(永远最新,零陈旧副本)。
- **按 id 请求**:`ctx.ask_service::<Req, Res>(svc, req, timeout)` —— 无人提供 → 立即 `UnknownService`;Req 型别与声明不符 → 发出前 `InvalidPayload`;`ask::<Req,Res>(target, …)` 按注册名直呼照旧可用。
- **每插件一个 actor**:**无界**(0.5.0:默认不丢)邮箱 + 专用 `ek-worker.<name>` 线程,`on_mail` 是插件唯一收信口,天然串行化。
- **动态生命周期(0.2.0)**:运行期可随时 `add_plugin`(同步装配即刻 `Running`)与 `remove_plugin`(五步关停 + 能力索引修剪);宿主广播 `ePlugUp / ePlugDown / eSrvChg`(固定键 + `.<名>` 后缀双发),订阅者据此**迟绑定**与**失败转移**。
- **目录改版(0.2.0)**:`ctx.publish_services(...)` —— 插件目录随配置/状态演化(先换 services() 再申报,纪律见 DESIGN §3)。
- **版本元数据(0.2.0)**:`Plugin::version()`(约定填 crate 版本);`PluginInfo / ProviderRef` 全披露面携带;多提供方共享同一 `svc_id` 即天然主备/失败转移。
- **快失败请求**:`ask` 失败面——目标不在(预检+断连,即时 `TargetGone`)、等满(`Timeout`)、契约不匹配(`InvalidPayload`);handler panic 时回包信道关闭 → 发起方立即感知。
- **故障熔断**:handler **连续** panic×3(处理成功即复位)→ `Quarantined` + `ePlugQDown.<name>` 结构化广播(0.3.0:唯一观测通道;宿主零日志);邻居插件毫发无损。
- **背压可观(0.5.0 口径反转)**:邮箱**无界 → 默认不丢**;深度实时可查
(`mail_stats().pending` 当前积压 / `peak` 历史峰值 / `soft_cap` 该插件自己的软顶);
**只有触到软顶(缺省 262144)才丢** —— 丢新帧 + `spilled` 计数 + 节流 stderr。
无界 ≠ 无限:软顶就是"不丢"的兜底出口,目的是不让慢消费者把进程吃挂。
- **五步关停**:卸 forwarder → `on_stop` → 关邮箱 → worker 排空 → join(带超时,超时告警绝不硬等)。
- **配置契约(0.4.0,additive)**:`section` 模块(`SectionMap`/`Field`/`Configurable`/`ApplyReport`)+ `Plugin::as_configurable`(`None` = 「无配置面」,存量插件零改动零负担)。持久配置一律走 `Configurable` + ek-cfg 秘面,**明面零秘值**;操作见 [`.dsh/skills/ek-plugin-dev.md`](.dsh/skills/ek-plugin-dev.md)。
## 示例
```rust
use ek_core::{MainEngine, Plugin, PluginContext, ServiceDesc, PlugNotice, PluginState};
use ek_core::{envelope::Envelope, AskError};
use ek_event::Event;
use std::borrow::Cow;
use std::time::Duration;
use std::sync::Arc;
// 业务契约类型(业务 crate 自备,ek-core 一无所知)
#[derive(Clone, Copy, Debug)] struct Ping { id: u64 }
#[derive(Clone, Copy, Debug)] struct Pong { id: u64 }
// 提供方:自报目录(细节住插件本身,宿主零复制)
struct Broker { name: String }
impl Plugin for Broker {
fn name(&self) -> &str { &self.name }
fn version(&self) -> Cow<'static, str> {
// 注意:必须显式覆写;trait 默认实现无法引用本 crate 的 pkg 版本
Cow::Borrowed(env!("CARGO_PKG_VERSION"))
}
fn routes(&self) -> Vec<Cow<'static, str>> {
vec![Cow::Owned(ek_core::envelope::e_req_type("broker"))]
}
fn services(&self) -> Vec<ServiceDesc> {
vec![ServiceDesc::describe::<Ping, Pong>("order.place", "下单并回 Pong")]
}
fn on_mail(&self, event: &Event) {
Envelope::handle::<Ping, Pong>(event, |p| Pong { id: p.id });
}
}
// 消费方:on_init 自主发现(不认识提供方注册名,按服务 id 找)
struct Client { name: String, provider: Option<String> }
impl Plugin for Client {
fn name(&self) -> &str { &self.name }
fn routes(&self) -> Vec<Cow<'static, str>> { vec![Cow::Borrowed("ePlugUp")] }
fn on_init(&self, ctx: &PluginContext) {
// 目录自商:拿到提供方名 + 方法说明;找不到则自选降级/迟绑定
if let Some(p) = ctx.find_service("order.place").into_iter().find(|p| p.state == PluginState::Running) {
self.provider = Some(p.name); // ctx 是唯一出口,自存的也必须是 ctx 派生物
}
}
fn on_mail(&self, event: &Event) {
// 收到 ePlugUp(新提供方上线)可自行决定是否切换 provider
}
}
fn main() {
let engine = MainEngine::new(None);
engine.add_plugin(Arc::new(Broker { name: "broker".into() })).unwrap();
engine.add_plugin(Arc::new(Client { name: "client".into(), provider: None })).unwrap();
engine.start().unwrap(); // 拓扑/注册序装配,start 与动态 add 共用 launch_one
// 运行期动态 add / remove 随时可做;ePlugUp/ePlugDown/eSrvChg 是宿主的全部吭声
engine.close_all(Duration::from_secs(2)); // 五步关停 + 停总线
}
```
## 纪律(协议层)
1. 收:只有 `on_mail` 一个入口;订阅面由 `routes()` 静态声明(运行期动态订阅不支持;生命周期广播已内置固定键可直接订)。
2. 发:只有 `ctx.put / ask / ask_service / notify / publish_services`(0.3.0:宿主零日志,`write_log` 出口移除;业务日志由插件自造载荷 `put`,业务链上用 ek-data 的 `LogData` 到 `"eLog"` 键);**ask/ask_service 禁止自呼**(自己等自己的回包会等到超时)。
3. 请求义务:`Envelope::handle` 成功回 `Ok(Res)`、契约错型回 `Err(InvalidPayload)`;handler panic 由断连信号传达。
4. 目录纪律:`services()` 必须纯/静态/无锁/不阻塞(A 型纪律),目录变更**先改内容再 `publish_services`**。
5. 宿主无业务方法;管理类操作(add/remove/start/close/remove)按串行使用设计。
> **写新插件?** 不背历史文档,直接照 [.dsh/skills/ek-plugin-dev.md](.dsh/skills/ek-plugin-dev.md) 走:七条纪律 + 配置契约四接口 + restore 链 + 受理三行序 + 零明文红线 + 收工 checklist。钉清单单源在 ek-station `DESIGN.md` §BD-CFG。
## 元信息
- crate:`ek-core 0.4.0`(edition 2024,`rust-version = 1.85`)
- 依赖:`ek-event = "0.1.2"`(crates.io)+ `crossbeam-channel`(邮箱/总线同源)
- 插件开发指导:[`.dsh/skills/ek-plugin-dev.md`](.dsh/skills/ek-plugin-dev.md)(skill 正源)
- 仓库:https://cnb.cool/enkeep/ek-core
- License:MIT(见 [LICENSE.txt](LICENSE.txt))
设计文档:[DESIGN.md](DESIGN.md);验收细节:[TESTPLAN.md](TESTPLAN.md);版本变更:[CHANGELOG.md](CHANGELOG.md)。