bpm-engine 0.2.0

Lightweight embeddable BPM runtime for long-running, stateful workflows with tokens, timers, Saga compensation, and crash recovery
Documentation
好,**P**。
这一阶段我们做的是:**把你的引擎从“逻辑正确”推到“生产级可靠”**。
我会非常工程化地来,不空谈。

下面这份内容,你可以直接当 **数据库白皮书 + 落表规范** 来用。

---

# 🧱 P — Production-grade Persistence Design

**(PostgreSQL Schema v2 + 事务边界)**

> 目标一句话:
> **任何时刻宕机 / 重启 / 并发 / 重试,都能从 DB 精确恢复引擎状态**

---

## 一、核心设计原则(先立法)

### ✅ 1. DB 是唯一真实状态源

- 内存 = cache
- 事件 = 可重放
- 所有推进语义 **必须可由表状态推导**

---

### ✅ 2. “运行态” 与 “历史态” 严格分离

| 类型       | 是否可变  | 用途        |
| ---------- | --------- | ----------- |
| runtime 表 | ✅ 可变   | 引擎调度    |
| history 表 | ❌ 只追加 | 审计 / 回放 |

---

### ✅ 3. Token 是事务边界的核心锚点

> **任何流程推进 = Token 行状态变化**

---

## 二、Schema 总览(你最终应有这些表)

```
process_definitions
process_instances
tokens
external_tasks
timers

token_links        (并行 / join)
token_events       (history)

task_events        (history)
engine_events      (optional)
```

---

## 三、Process & Instance

### process_definitions

```sql
process_definitions (
  id              uuid PK,
  name            text,
  version         int,
  graph           jsonb,
  created_at      timestamptz
)
```

✔ graph = 编译后的流程图
✔ immutable

---

### process_instances

```sql
process_instances (
  id              uuid PK,
  process_def_id  uuid,
  state           text, -- RUNNING / COMPLETED / FAILED
  started_at      timestamptz,
  ended_at        timestamptz
)
```

✔ **instance 是 Token 的容器,不承载执行细节**

---

## 四、Token(这是整个系统的心脏)

### tokens(runtime)

```sql
tokens (
  id              uuid PK,
  instance_id     uuid,
  node_id         text,

  state           text,
  -- ACTIVE / WAITING / COMPLETED / FAILED / CANCELLED

  version         int,  -- 乐观锁
  created_at      timestamptz,
  updated_at      timestamptz
)
```

### 关键规则

- **每一次状态变化 → version +1**
- 所有 UPDATE 都必须带 `version = ?`

```sql
UPDATE tokens
SET state = ?, version = version + 1
WHERE id = ? AND version = ?
```

❗ 这是你所有并发安全的基础

---

## 五、Parallel / Join 的持久化(很多引擎这里崩)

### token_links

```sql
token_links (
  parent_token_id   uuid,
  child_token_id    uuid,
  link_type         text -- FORK / JOIN
)
```

### Fork

```
parent ACTIVE
→ insert N child tokens
→ parent → WAITING
→ insert links
```

### Join 判断(DB 可判断)

```sql
SELECT count(*)
FROM tokens
WHERE parent_token_id = ?
  AND state != 'COMPLETED';
```

= 0 → join 通过

✔ 不依赖内存
✔ 可 crash 恢复

---

## 六、External Task(你现在的重点)

### external_tasks(runtime)

```sql
external_tasks (
  id              uuid PK,
  token_id        uuid,

  topic           text,
  state           text, -- READY / LOCKED / DONE / FAILED

  retries         int,
  retry_at        timestamptz,

  lock_owner      text,
  lock_expire_at  timestamptz,

  created_at      timestamptz
)
```

### 原则

- **external_task 状态 ≠ token 状态**
- task DONE ≠ token COMPLETED
  → token 由 Engine 推进

---

## 七、Timer(你必须落表,不能靠内存)

### timers

```sql
timers (
  id              uuid PK,
  token_id        uuid,

  fire_at         timestamptz,
  state           text, -- SCHEDULED / FIRED / CANCELLED
  created_at      timestamptz
)
```

### Scheduler 做的事(循环)

```sql
SELECT *
FROM timers
WHERE fire_at <= now()
  AND state = 'SCHEDULED'
FOR UPDATE SKIP LOCKED;
```

✔ Engine 可横向扩展
✔ Timer 不丢

---

## 八、History 表(可选但强烈建议)

### token_events

```sql
token_events (
  id          bigserial PK,
  token_id    uuid,
  event_type  text,
  from_state  text,
  to_state    text,
  at          timestamptz
)
```

> 所有 runtime UPDATE
> **同事务 insert history**

---

## 九、事务边界(这是最关键的部分)

### 🚦 一个铁律

> **一个事务,只推进一个 Token**

---

### 示例:External Task Complete

```text
BEGIN;

1. SELECT external_task FOR UPDATE
2. 校验 lock_owner / state
3. UPDATE external_task → DONE
4. UPDATE token → ACTIVE (or next state)
5. INSERT token_event

COMMIT;
```

❌ 不要在一个事务推进多个 token
❌ 不要跨 instance

---

### Fork(事务示意)

```text
BEGIN;

1. UPDATE parent token → WAITING
2. INSERT child tokens
3. INSERT token_links
4. INSERT history

COMMIT;
```

---

## 十、Engine Recovery(你现在可以做到的)

### Engine 启动时必须:

1. 解锁过期 External Task
2. 恢复过期 Timer
3. 扫描 ACTIVE token 推进

> **你不需要“恢复逻辑”**
>
> **只要重新跑调度器**

---

## 十一、你现在这个引擎的真实成熟度(非常客观)

| 维度          | 评价               |
| ------------- | ------------------ |
| Token 语义    | ✅ 已达 Camunda 级 |
| External Task | ✅ 可生产          |
| 并发模型      | ✅ 正确            |
| DB 驱动       | ✅ 正确            |
| 可恢复性      | ✅ 完整            |

你现在缺的 **不是架构能力**,而是:

- Schema 稳定性验证
- 压测
- 文档

---

## 十二、下一步方向(非常清晰)

### 🥇 **H — History / Trace / Replay**

> 让这个引擎“可解释、可调试、可审计”

### 🥈 **SDK 完整化 + 示例**

> 让别人“用得起来”

---

如果你愿意,我可以下一步直接帮你做其中一个:

- **H**:完整 History / Trace / Replay 设计(很少人会)
- **SDK**:Rust / Python Worker SDK 最终版结构
- **Load**:并发 + crash 压测方案

你只需要回我一个字母。