ek-core 0.4.1

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


**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**:bounded(默认 1024,可覆写)邮箱 + 专用 `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:唯一观测通道;宿主零日志);邻居插件毫发无损。
- **背压可观**:邮箱满即丢弃计数(节流 stderr 提示;结构化事实 `mail_stats()` 随时可查)。
- **五步关停**:卸 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)。