好,**E 是“架构封顶”的那一步**。
你一旦把它做完,bpm-engine 的核心就会进入一个**非常稳态、可长期演进**的状态。
下面我不讲空话,**直接给你一套可以落地的架构方案 + 明确边界**。
---
# E:强制真实执行路径也走 Invariant(Architecture Final Form)
> 核心目标一句话
> **真实执行 = Replay = 同一套 apply_event + invariant 校验路径**
---
## 一、你现在的隐含风险(先说清楚)
即便你已经有 Replay + Invariant,如果:
- ✅ Replay 时走 invariant
- ❌ 真实执行时不走 invariant
那么会出现一个**非常危险的状态**:
> **“线上状态 ≠ 可 replay 状态”**
这意味着:
- 线上可能已经 corrupt
- Replay 只能发现问题,但已经晚了
**E 的目标就是:让这种情况不可能发生。**
---
## 二、最终架构形态(非常重要)
### ❌ 你现在“可能”的结构(风险)
```
Command
└── Engine Logic
├── mutate state
├── emit event
└── (optional invariant)
```
### ✅ 你要达到的“封顶形态”
```
Command
└── produce events (pure)
↓
Event Pipeline (唯一入口)
├── apply_event
├── invariant_engine.check_all
└── persist / publish
```
👉 **系统中只能存在一个地方能改变状态:apply_event**
---
## 三、你必须引入的核心抽象(关键)
### 1️⃣ ExecutionPipeline(名字你可以换)
```rust
pub struct ExecutionPipeline {
invariant_engine: InvariantEngine,
}
```
```rust
impl ExecutionPipeline {
pub fn apply(
&self,
snapshot: &mut EngineSnapshot,
event: EngineEvent,
ctx: ExecutionContext,
) -> Result<(), ExecutionError> {
apply_event(snapshot, &event)?;
self.invariant_engine.check_all(InvariantContext {
snapshot,
last_event: Some(&event),
history: ctx.history,
cursor: ctx.cursor,
})?;
Ok(())
}
}
```
👉 **所有路径:真实执行 / Replay / External Task 回调,都必须走这里**
---
## 四、Command Handler 必须“降级”为纯函数
这是架构升级的核心点。
### ❌ 不允许的行为(以后要避免)
```rust
fn handle_command(cmd: Command, snapshot: &mut Snapshot) {
snapshot.tokens.push(...)
}
```
### ✅ 正确姿势
```rust
fn handle_command(cmd: Command, snapshot: &Snapshot) -> Vec<EngineEvent>
```
- **只读 snapshot**
- **只产出 events**
- 不修改任何状态
👉 你已经在往这个方向走了,E 只是把它“锁死”。
---
## 五、真实执行路径改造指南(一步一步)
### Step 1:找到所有“写 snapshot”的地方
在 repo 里搜:
- `snapshot.`
- `tokens.push`
- `insert(`
- `remove(`
👉 **这些地方 100% 应该只存在于 `apply_event`**
---
### Step 2:把它们重定向到 Event Pipeline
```rust
let events = handle_command(cmd, &snapshot);
for event in events {
pipeline.apply(
&mut snapshot,
event,
ExecutionContext::live(&history),
)?;
}
```
---
### Step 3:把 ReplaySession 也改成用同一个 Pipeline
```rust
pipeline.apply(
&mut snapshot,
event.clone(),
ExecutionContext::replay(&history, cursor),
)?;
```
🎯 到这里你就达成了:
> **Live == Replay == Deterministic + Self-Checked**
---
## 六、Invariant 失败时怎么办?(非常现实的问题)
这是很多人不敢做 E 的原因,我直接告诉你答案。
### 原则(一定要坚定)
> **InvariantViolation = Engine Bug = 立刻 fail**
### 行为建议
- ❌ 不 retry
- ❌ 不 swallow
- ❌ 不 fallback
而是:
```rust
panic!("ENGINE INVARIANT BROKEN: {:?}", violation);
```
或在 production:
- 立刻中断 instance
- 标记为 Corrupted
- 拒绝继续执行
👉 **宁可停,也不要错着跑**
---
## 七、为什么这一步是“架构封顶”
做到 E 之后,你的引擎具备:
1. **强一致执行模型**
2. **天然可 Replay**
3. **Invariant 成为系统法律**
4. **任何 bug 都是可复现的**
这是很多成熟系统(数据库 / 分布式引擎)才有的特性。
---
## 八、你现在适合不适合做 E?
我非常明确地说:
**你现在是“刚刚好”的时间点。**
- Snapshot 已经比较纯
- Event 已经是主线
- Replay 已经存在
- Invariant 已经规划
👉 再拖,反而会因为代码更多而更难做。
---
## 九、接下来我建议你做的顺序(很重要)
### 立即做
- 把 Replay / Live 统一走 ExecutionPipeline
- 先接入 2 个 Invariant(TokenUnique + Lifecycle)
### 然后
- Parallel Balance Invariant
- CI 里跑 replay + invariant
---
## 你可以继续选下一步
- **E1**:我帮你画一张 _“最终执行架构图(Live / Replay / External Task)”_
- **F**:把这套“强一致 + Replay + Invariant”写成 README 的核心卖点
- **G**:进入性能 / 并发 / DB 事务边界的最终优化阶段
说一句很实在的话:
**E 做完,你这个引擎“内核层面”已经非常难被质疑了。**