Skip to main content

helix_core/
engine.rs

1//! # engine.rs
2//!
3//! `ExecutionShell`——sans-IO 确定性执行壳(聚合根)。
4//!
5//! ## 核心不变量
6//!
7//! - `step()` 严格同步:零 await,零 I/O,零 spawn
8//! - `step()` 纯函数语义:相同 Tick 序列 + 相同模块初态 ⇒ 相同 Effect 序列
9//! - 维护 `corr_map` 和 `timer_map`,保证 PortReply/Timer 定向投递(审查意见2 P3)
10
11use crate::effect::{Effect, EffectSink};
12use crate::error::CoreError;
13use crate::module_host::{CorrelationMap, ModuleHost, TimerMap};
14use crate::tick::Tick;
15
16/// sans-IO 确定性执行壳。
17///
18/// ## 使用方式(三端通用)
19///
20/// ```rust,ignore
21/// let mut shell = ExecutionShell::new();
22/// shell.register(ImModule::new(config));
23/// shell.start()?;
24///
25/// // 事件泵(由 driver 的 engine_loop.rs 驱动)
26/// let effects = shell.step(Tick::Inbound(bytes), now_ms)?;
27/// // driver 兑现 effects,把结果包成下一个 Tick 喂回
28/// ```
29pub struct ExecutionShell {
30    host: ModuleHost,
31    /// Correlation → module_index(PortReply 定向路由)
32    corr_map: CorrelationMap,
33    /// TimerId → module_index(Timer 定向路由)
34    timer_map: TimerMap,
35    /// 跨 step 复用的 Effect 缓冲(零分配,轴①)
36    scratch: EffectSink,
37}
38
39impl ExecutionShell {
40    pub fn new() -> Self {
41        Self {
42            host: ModuleHost::new(),
43            corr_map: CorrelationMap::default(),
44            timer_map: TimerMap::default(),
45            scratch: EffectSink::new(),
46        }
47    }
48
49    /// 注册业务模块(加模块不改此函数,开闭原则)
50    pub fn register(&mut self, m: impl crate::module_host::Module + 'static) {
51        self.host.register(m);
52    }
53
54    /// 启动所有已注册模块(调用 on_start),返回初始 Effect 列表。
55    ///
56    /// driver 在创建完 ExecutionShell 并注册所有模块后调用一次。
57    ///
58    /// ## BLK-1a 修复
59    ///
60    /// 逐模块调用 on_start 并立即 `register_effects_to_maps(idx)`,
61    /// 确保每个模块的 Correlation/TimerId 注册到正确的 module_index。
62    pub fn start(&mut self) -> Result<&[Effect], CoreError> {
63        self.scratch.clear();
64        let module_count = self.host.module_count();
65        for idx in 0..module_count {
66            // EFFECT-3:start() 跨模块**不 clear** scratch(要累积返回所有模块的 on_start
67            // Effect),故每个模块只能注册自己**新增**区间 [start..],否则后一个模块的
68            // register 会把前序模块的 Correlation 重复扫描并覆盖到自己 → PortReply 投错家。
69            let start = self.scratch.as_slice().len();
70            self.host.start_one(idx, &mut self.scratch)?;
71            self.register_effects_to_maps(idx, start);
72        }
73        Ok(self.scratch.as_slice())
74    }
75
76    /// 停止所有已注册模块(调用 on_stop),返回清理 Effect 列表。
77    pub fn stop(&mut self) -> Result<&[Effect], CoreError> {
78        self.scratch.clear();
79        self.host.stop_all(&mut self.scratch)?;
80        Ok(self.scratch.as_slice())
81    }
82
83    /// **核心方法**:处理一个 Tick,返回本次 step 产出的 Effect 切片。
84    ///
85    /// ## 返回值生命周期
86    ///
87    /// 返回 `&[Effect]` 借用 `self.scratch`,在下一次 `step()` 调用前有效。
88    /// driver 必须在下一次 `step()` 之前消费完(或 clone)这些 Effect。
89    ///
90    /// ## 路由策略
91    ///
92    /// - `Inbound` / `Command`:线性扫描 `accepts()`
93    /// - `PortReply`:corr_map O(1) 定向投递并消费
94    /// - `PortProgress`:corr_map O(1) 定向投递但不消费
95    /// - `Timer`:timer_map O(1) 定向投递
96    ///
97    /// ## BLK-1a 修复
98    ///
99    /// `dispatch_inbound` 返回的 `Some(idx)` 现在被正确传入 `register_effects_to_maps`,
100    /// 替换了原来的 `let module_index = 0usize; // placeholder` 占位符。
101    /// 这确保多模块场景中,Correlation/TimerId 被注册到正确的 module_index,
102    /// 而不是错误地路由到 module[0]。
103    pub fn step(&mut self, tick: Tick, now_ms: u64) -> Result<&[Effect], CoreError> {
104        self.scratch.clear();
105
106        match &tick {
107            Tick::Inbound(_) | Tick::Command(_) | Tick::Connected(_) | Tick::Disconnected(_) => {
108                match self
109                    .host
110                    .dispatch_inbound(&tick, now_ms, &mut self.scratch)?
111                {
112                    Some(idx) => {
113                        // BLK-1a: 传入真实的 module_index,而非 0 占位符
114                        // start=0:step() 开头已 clear,scratch 内全是本次新增(EFFECT-3)
115                        self.register_effects_to_maps(idx, 0);
116                    }
117                    None => {
118                        // 无模块处理此 Inbound/Command——可能是正常(如未知帧 drop),
119                        // 也可能是配置错误。此处不报错,由调用方决定日志级别。
120                        tracing::debug!("no module accepted tick");
121                    }
122                }
123                // on_start 也可能产出 Effect,返回前确保 scratch 已被注册
124                // (on_start 由 start() 单独调用,此处仅处理 step 路径)
125                return Ok(self.scratch.as_slice());
126            }
127
128            Tick::PortReply { corr, .. } => {
129                match self.corr_map.consume(corr) {
130                    Some(module_index) => {
131                        self.host
132                            .dispatch_reply(module_index, &tick, now_ms, &mut self.scratch)?;
133                        // BLK-1a: PortReply 产出的新 Effect 注册到发出原 corr 的同一模块
134                        self.register_effects_to_maps(module_index, 0);
135                    }
136                    None => {
137                        // corr 未知:可能是 PersistFire 后的误发(不应发生),
138                        // 或模块已停止后的迟到响应(可接受,静默忽略)
139                        tracing::warn!(
140                            corr = corr.raw(),
141                            "received PortReply for unknown correlation, ignoring"
142                        );
143                    }
144                }
145                return Ok(self.scratch.as_slice());
146            }
147
148            Tick::PortProgress { corr, .. } => {
149                match self.corr_map.peek(corr) {
150                    Some(module_index) => {
151                        self.host
152                            .dispatch_reply(module_index, &tick, now_ms, &mut self.scratch)?;
153                        self.register_effects_to_maps(module_index, 0);
154                    }
155                    None => {
156                        // 终态已消费或从未注册的迟到进度是可接受噪音。
157                        tracing::debug!(
158                            corr = corr.raw(),
159                            "received PortProgress for unknown correlation, ignoring"
160                        );
161                    }
162                }
163                return Ok(self.scratch.as_slice());
164            }
165
166            Tick::Timer(timer_id) => {
167                match self.timer_map.peek(timer_id) {
168                    Some(module_index) => {
169                        // one-shot-on-fire 语义(HIGH 修复,2026-06-22):timer 触发即作废。
170                        // 全仓无 recurring timer——"周期心跳"由模块吐**新 id** 的 ScheduleTimer
171                        // re-arm(如 IM ping),新 id 经 register_effects_to_maps 注册新条目,
172                        // 绝不复用旧 id。故 fire 后必须回收已触发条目,否则 timer_map 单调泄漏。
173                        self.host
174                            .dispatch_reply(module_index, &tick, now_ms, &mut self.scratch)?;
175                        // 不变量:必须先 remove(已触发 id)、后 register 新 effect。
176                        // re-arm 用全新 id(不复用 timer_id)→ 两步互不干扰;
177                        // 若极端情况下 re-arm 复用了同一 id(当前全仓无此模式),
178                        // 此顺序会误删 —— 故顺序固定,新语义须显式 RecurringTimer 而非复用 id。
179                        self.timer_map.remove(timer_id);
180                        // Timer 产出的新 Effect(如 re-arm 的新 id ScheduleTimer)注册到同一模块
181                        self.register_effects_to_maps(module_index, 0);
182                    }
183                    None => {
184                        // timer_id 未知:已被 CancelTimer 移除后的迟到触发,静默忽略
185                        tracing::debug!(
186                            id = timer_id.raw(),
187                            "received Timer for unknown id, ignoring"
188                        );
189                    }
190                }
191                return Ok(self.scratch.as_slice());
192            }
193        }
194    }
195
196    /// 扫描 scratch `[start..]` 区间内本次 dispatch 产出的新 Effect,
197    /// 把 Correlation/TimerId 注册到 `module_index` 对应的 map。
198    ///
199    /// ## BLK-1a 修复
200    ///
201    /// 参数 `module_index` 来自 dispatch 函数的返回值,不再是硬编码的 `0`。
202    /// 确保多模块并存时,每个模块的 Correlation/TimerId 路由到自己而非 module[0]。
203    ///
204    /// ## EFFECT-3 修复
205    ///
206    /// 参数 `start` = 本次 dispatch 前的 scratch 长度,只扫**新增**区间。
207    /// `step()` 开头 clear 故传 `0`;`start()` 跨模块累积 scratch 故传各模块 on_start 前的长度,
208    /// 避免后一个模块把前序模块的 Correlation 重复扫描并覆盖到自己(PortReply 投错家)。
209    fn register_effects_to_maps(&mut self, module_index: usize, start: usize) {
210        for effect in &self.scratch.as_slice()[start..] {
211            match effect {
212                Effect::Persist { corr, .. }
213                | Effect::PersistAtomic { corr, .. }
214                | Effect::Http { corr, .. }
215                | Effect::UploadFile { corr, .. }
216                | Effect::Request { corr, .. } => {
217                    self.corr_map.register(*corr, module_index);
218                }
219                Effect::ScheduleTimer { id, .. } => {
220                    self.timer_map.register(*id, module_index);
221                }
222                Effect::CancelTimer { id } => {
223                    self.timer_map.remove(id);
224                }
225                // HttpFire 故意不注册 corr:它无 Correlation、不产 PortReply(fire-and-forget),
226                // 与 Emit / PersistFire 同列落通配臂。
227                _ => {}
228            }
229        }
230    }
231}
232
233impl Default for ExecutionShell {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239#[cfg(test)]
240impl ExecutionShell {
241    /// 测试探针:当前 timer_map 路由条目数。
242    ///
243    /// 用于回放测试断言「连续 N 次 Timer 触发后 timer_map 不随 N 线性增长」
244    /// (one-shot 收敛到 0 / re-arm 新 id 稳定到 1,绝非 ==N)。
245    /// `#[cfg(test)]` 隔离,不污染生产 API(不破坏 public-api 闸门)。
246    pub(crate) fn timer_map_len(&self) -> usize {
247        self.timer_map.len()
248    }
249}
250
251// ─── 确定性单元测试(不依赖任何运行时)──────────────────────────────────────
252
253#[cfg(test)]
254#[path = "engine_tests.rs"]
255mod tests;