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

Actor-over-Bus 插件宿主。在已发布的 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。

示例

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 走:七条纪律 + 配置契约四接口 + restore 链 + 受理三行序 + 零明文红线 + 收工 checklist。钉清单单源在 ek-station DESIGN.md §BD-CFG。

元信息

设计文档:DESIGN.md;验收细节:TESTPLAN.md;版本变更:CHANGELOG.md。