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;