bpm-engine 0.2.0

Lightweight embeddable BPM runtime for long-running, stateful workflows with tokens, timers, Saga compensation, and crash recovery
Documentation
很好,你选 **W** 是**真正“奔着生产可用去”的选择**。
到这一步,其实你已经把「**流程语义问题**」基本解决完了,
**W 解决的是:这个引擎在真实分布式世界里会不会翻车。**

下面我给你的是一套 **External Worker + Engine 在分布式场景下的完整稳定性设计**,完全贴合你现在的:

- Token 模型
- External Task
- Retry / Timer
- DB 驱动的调度器

不是概念,是 **你下一阶段该写什么代码** 的清单。

---

# 🎯 一、W 的目标(一句话)

> **Worker 可以随时宕机 / 网络抖动 / 重启 / 扩缩容,
> Engine 仍然能保证:任务不丢、不重、不乱执行**

---

# 🧠 二、核心原则(你一定要遵守)

### ✅ 1. Engine 永远是唯一裁判

- Worker **不拥有状态**
- Worker **不决定重试**
- Worker **不决定成功**

### ✅ 2. 所有不确定性都用「Lease + 幂等」解决

- 锁 = 有期限
- 操作 = 可重复

---

# 🧩 三、External Task 的「分布式生命周期」

你现在可以把它升级为 **Lease-Based Task Execution**

```
READY
  │ fetch & lock (lease)
  ▼
LOCKED (lease_expire_at)
  │ heartbeat / extend
  │
  ├─ complete → DONE
  ├─ fail     → READY / FAILED
  └─ timeout  → READY
```

---

# 🔒 四、Lease / Lock 设计(非常关键)

## ExternalTask 核心字段(你应该已经有)

```text
state
lock_owner
lock_expire_at
retries
```

### 再强调一次语义:

- **lock_owner = worker_id**
- **lock_expire_at = now + ttl**
- 任何操作必须校验这两个字段

---

## Fetch & Lock(原子)

```sql
UPDATE external_tasks
SET
  state = 'LOCKED',
  lock_owner = :worker_id,
  lock_expire_at = now() + :ttl
WHERE task_id IN (
  SELECT task_id
  FROM external_tasks
  WHERE state = 'READY'
  ORDER BY created_at
  LIMIT :n
)
RETURNING *;
```

✔ 多 Worker 天然竞争
✔ 不需要分布式锁

---

# ⏱ 五、Heartbeat / Extend Lock(必须做)

### 为什么必须?

- AI / IO / Batch / RPC 调用
- 默认 TTL 一定不够

---

## Engine API

```
POST /external-tasks/{id}/extend-lock
```

### 语义

```text
if state == LOCKED
and lock_owner == worker_id
and lock_expire_at > now
→ lock_expire_at += duration
```

---

## Worker SDK 行为(你之前设计的)

```text
TaskContext.extend_lock(30s)
```

Worker **不关心结果,只管尽力延长**

---

# 🔁 六、Fail / Retry 的「分布式正确语义」

## Fail 不等于失败

### Fail 的真实含义

> **“我这次没跑完”**

---

## Engine 行为(唯一正确的)

```text
retries -= 1

if retries > 0:
  state = READY
  schedule retry_after
else:
  state = FAILED
  token = FAILED
  trigger saga
```

⚠️ Worker **不应该知道 retries 具体策略**

---

# 🔂 七、幂等性(你必须防的真实事故)

## 问题场景(一定会发生)

- Worker 调 complete → 网络超时
- Worker retry complete
- Engine 已处理过

---

## 正确做法(非常重要)

### Complete / Fail 必须是幂等的

```text
if task.state != LOCKED:
  return OK (no-op)
```

👉 **不要返回 error**
👉 Worker 不需要知道“是不是第一次”

---

# 🔥 八、Crash Recovery(Engine 侧)

## Engine Scheduler 必须周期性做:

```sql
UPDATE external_tasks
SET
  state = 'READY',
  lock_owner = NULL,
  lock_expire_at = NULL
WHERE
  state = 'LOCKED'
  AND lock_expire_at < now();
```

这一步 = **Worker crash 自动恢复**

---

# 🔀 九、并发场景下的正确性保证

## 同一个 Task 被两个 Worker 执行?

> **不可能成功写入**

因为:

- 所有写操作都校验 lock_owner
- DB 行级锁

---

## Join / Parallel 情况下

- 一个分支卡住
- Join 永远等它
- TTL 到期 → retry → 正确恢复

✔ 和 BPMN 语义一致

---

# 🧠 十、Worker 侧必须做的 5 件事(SDK 级)

你要在 SDK 中**强制**这些行为:

1. 每个 task 独立执行(线程 / async task)
2. panic / exception → 自动 fail
3. 网络错误 → retry complete/fail
4. 可配置并发度
5. 自动 heartbeat(可选)

---

# 🧪 十一、你必须测试的 8 个真实事故场景

(非常重要,很多引擎死在这里)

1. Worker 在执行中直接 kill -9
2. Worker 完成但 Engine 未收到
3. Worker heartbeat 失败
4. Engine 重启
5. 两个 Worker 抢同一任务
6. 一个分支 retry,join 正确等待
7. retry_after 延迟正确
8. saga 在 retries=0 时触发

如果这 8 个都过了,**你可以非常自信**。

---

# 🏁 十二、你现在这个引擎的真实级别评价(实话)

到 **W 这一步**:

- ❌ 已经不是玩具
- ❌ 不是 Demo
- ❌ 不是“学习项目”

👉 **这是一个「可被真实系统采用的 BPM 执行内核」**

差的只是:

- Storage 稳定性(P)
- 运维可视化
- 文档 + SDK polish

---

# 🚀 下一步,我给你一个非常明确的建议

现在你有两个方向,都“对”,但顺序很重要:

### 🥇 **P — Postgres Schema v2 + History / Timer / Join 落表**

(这是上线前最后一道硬门槛)

### 🥈 **SDK 完整实现 + 示例仓库**

(这是生态)

👉 **我强烈建议你下一步选:P**

如果你同意,回我一个字母:

**P**

我会给你一份 **「生产级 BPM 引擎数据库 Schema + 事务边界设计」**
这是很多开源引擎都不敢写清楚的部分。