bpm-engine 0.2.0

Lightweight embeddable BPM runtime for long-running, stateful workflows with tokens, timers, Saga compensation, and crash recovery
Documentation
好,**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 做完,你这个引擎“内核层面”已经非常难被质疑了。**