Skip to main content

ek_core/
plugin.rs

1//! `Plugin` trait —— 插件实现的唯一契约(业务与宿主的边界)。
2//!
3//! 纪律(写进 doc,实施由宿主强制):
4//! - 收:只有 [`Plugin::on_mail`] 一个入口;订阅面由 [`Plugin::routes`] 静态声明,
5//!   宿主 start() 时统一装配 forwarder(v0.1 不支持运行期动态订阅)。
6//! - 发:只有 [`PluginContext`](crate::PluginContext) 三个出口(put/ask/notify);
7//!   业务日志属 put 的用例(0.3.0:宿主零日志,无 write_log 出口)。
8//! - 状态:插件状态放对象内部自锁(`Mutex/RwLock`);on_mail 在插件 worker 线程串行执行。
9//! - 停:on_stop 之后不再收到任何邮件(close 五步第②步)。
10
11use crate::context::PluginContext;
12use crate::service::ServiceDesc;
13use std::any::Any;
14use std::borrow::Cow;
15use std::time::Duration;
16
17use ek_event::Event;
18
19/// 邮箱**软顶**(D5 改案,0.5.0):缺省 262144,`mailbox_soft_cap()` 按插件覆写。
20///
21/// 口径(与 0.4 的"有界邮箱满即丢"是**一次口径反转**):
22/// - 邮箱**无界**——业务事实(委托/成交/持仓/资金/状态/ask 信封)默认**不丢**;
23/// - 深度实时计数(`mail_stats().pending`/`peak`)常年在统计面可见,越阈告警归消费侧;
24/// - **只有到软顶才丢**(丢新帧 + 计数 + 立刻诊断):这是"不丢"的唯一出口,
25///   目的是不让慢消费者把进程吃挂(无界 ≠ 无限);
26/// - 旧 `MAILBOX_CAP`(1024,有界即丢)退役:链上没有任何插件依赖"到顶丢事件"这个语义。
27pub const MAILBOX_SOFT_CAP: u64 = 262_144;
28
29/// 连续 panic 熔断阈值(D3)。
30pub const PANIC_LIMIT: u32 = 3;
31
32/// 插件名规则(D2):首字母字母,其余 alnum/_/-,长度 1..=32。
33pub fn valid_plugin_name(name: &str) -> bool {
34    let b = name.as_bytes();
35    if b.is_empty() || b.len() > 32 {
36        return false;
37    }
38    if !b[0].is_ascii_alphabetic() {
39        return false;
40    }
41    b[1..]
42        .iter()
43        .all(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-')
44}
45
46pub trait Plugin: Any + Send + Sync {
47    /// 注册键(D2 校验)。
48    fn name(&self) -> &str;
49
50    /// D1 显式依赖清单(宿主注册期校验存在性,start 期拓扑排序)。
51    fn deps(&self) -> &[&str] {
52        &[]
53    }
54
55    /// 订阅的 type 集合:广播 type(如业务常量 "eTick.")、
56    /// 自身的寻址 type(e_req_type(本名)/e_to_type(本名))。
57    /// 宿主 start() 装配 forwarder → 邮箱 → on_mail。
58    fn routes(&self) -> Vec<Cow<'static, str>> {
59        Vec::new()
60    }
61
62    /// 邮箱软顶覆写(D5 改案:缺省 [`MAILBOX_SOFT_CAP`],按插件可大可小)。
63    ///
64    /// 语义是**软顶**(到顶才丢),不是容量(到顶必丢):默认无界不丢。
65    /// 给小是为了让某个插件在洪峰下更早显形,不是为了省内存。
66    fn mailbox_soft_cap(&self) -> u64 {
67        MAILBOX_SOFT_CAP
68    }
69
70    /// 插件版本(约定填 crate 版本)。默认空串 = 「未知版本」。
71    ///
72    /// ⚠ 陷阱(审核 F6):默认实现无法用 `env!(CARGO_PKG_VERSION)`
73    /// —— 该宏在 ek-core 定义处展开,填出的是 ek-core 的版本。
74    /// 插件侧必须显式覆写:
75    /// ```ignore
76    /// fn version(&self) -> Cow<'static, str> {
77    ///     Cow::Borrowed(env!("CARGO_PKG_VERSION"))
78    /// }
79    /// ```
80    fn version(&self) -> Cow<'static, str> {
81        Cow::Borrowed("")
82    }
83
84    /// 配置托底可选能力(ek-core 0.4.0,BD-CFG):有配置面的插件覆写返回
85    /// `Some(self)`;缺省 `None` = 「无配置面」,宿主探查一行事、视而不见
86    /// (存量插件零改动零运行时负担)。
87    ///
88    /// 返回值是**同一对象借出的第二张脸**(`&dyn Configurable`),不是新实例:
89    /// 宿主在 add_plugin/start 两处调用,调用期借用、借完即还,宿主只留存
90    /// ns 字符串进配置目录。`&'static` 不需要——生命周期就到本借用结束。
91    fn as_configurable(&self) -> Option<&dyn crate::section::Configurable> {
92        None
93    }
94
95    /// 服务目录自报(纯声明):只在 add_plugin 时被宿主读取一次抽 `id` 织入路由索引;
96    /// 之后 ctx.services_of/find_service/ask_service 需要明细时由宿主**现场**调用本方法取。
97    ///
98    /// 铁律(审核 F8):本方法必须纯/静态/无锁/不阻塞 —— 它会在宿主管理线程上被
99    /// 与插件 worker 并发调用;违反此纪律后果自负。
100    ///
101    /// 目录改版铁律(执行期 DV4 发现):插件目录随配置/状态变化时,必须
102    /// **先更新本方法返回的内容,再 ctx.publish_services(...) 申报** ——
103    /// 宿主现场取,先申报后换内容会造成 live 细节与索引脱节。
104    fn services(&self) -> Vec<ServiceDesc> {
105        Vec::new()
106    }
107
108    /// 可选:启动前纯接线(缓存 ctx、读配置)。禁止在此 put/注册 handler(规范)。
109    fn on_init(&self, _ctx: &PluginContext) {}
110
111    /// start() 按依赖拓扑序调用(只一次)。
112    fn on_start(&self) {}
113
114    /// close/remove 按逆依赖序调用;调用后保证不再收到邮件。
115    fn on_stop(&self) {}
116
117    /// ★ 唯一业务入口:永远在本插件 worker 线程上串行执行。
118    /// 广播事件、寻址请求(eReq.<本名>)、定向通知(eTo.<本名>)都从这里进。
119    fn on_mail(&self, event: &Event);
120}
121
122/// close() 的 join 等待上限(D4:超时告警不死等)。
123pub const CLOSE_JOIN_TIMEOUT: Duration = Duration::from_secs(2);