OFFLINE LAB / 合成场景

任务停了,
下一步由谁接手?

拨动模拟时钟,观察租约、generation 与未知结果如何决定下一次操作。

每一步来自构建时实际执行的 SQLite Jobs 模型。浏览器回放记录;没有网络调用、真实外部动作或 Cloudflare 实测。

五条真实执行路径

交互回放需要 JavaScript;以下步骤在构建时由同一模型执行,始终可阅读。

01 · 正常完成

登记、领取、完成各有自己的状态。 done 之后重复领取仍被拒绝。

  1. t=0 任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。
  2. t=1 A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。
  3. t=8 A 提交 done:True;done / generation 1。返回 True。generation 匹配且 t=8 < 31,结果被接受,租约清零。
  4. t=10 B 尝试领取:None;done / generation 1。返回 None。done 不在可领取状态中,重复到达不会再执行。
02 · 中断与接管

worker A 退出,数据库留住了租约。 租约到期允许接管;到期并不证明旧 worker 已停止。

  1. t=0 任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。
  2. t=1 A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。
  3. t=4 worker 退出,连接重开:None;running / generation 1。合成 worker A 中断;关闭后重开同一个 SQLite 文件。running、generation 1 与租约都还在。
  4. t=30 B 尝试领取:None;running / generation 1。返回 None。t=30 < 31,仍在租约内;数据库拒绝再次领取。
  5. t=31 B 尝试领取:2;running / generation 2。返回 generation 2。边界 t=31 满足 lease_until <= now,B 接管并获得截止 t=61 的新租约。
  6. t=35 B 提交 done:True;done / generation 2。返回 True。generation 2 匹配且新租约有效,恢复后的本地工作完成。
  7. t=40 A 尝试领取:None;done / generation 2。返回 None。完成后的重复领取不会再执行。
03 · 迟到的旧结果

新 generation 接管后,旧 worker 回来提交。 generation 保护本地提交;旧持有者不能覆盖新一轮结果。

  1. t=0 任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。
  2. t=1 A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。
  3. t=4 worker 退出,连接重开:None;running / generation 1。合成 worker A 中断;关闭后重开同一个 SQLite 文件。running、generation 1 与租约都还在。
  4. t=30 B 尝试领取:None;running / generation 1。返回 None。t=30 < 31,仍在租约内;数据库拒绝再次领取。
  5. t=31 B 尝试领取:2;running / generation 2。返回 generation 2。边界 t=31 满足 lease_until <= now,B 接管并获得截止 t=61 的新租约。
  6. t=32 A 提交 done:False;running / generation 2。返回 False。旧 generation 1 与当前 2 不匹配;状态仍为 running,B 的结果没有被覆盖。
  7. t=35 B 提交 done:True;done / generation 2。返回 True。generation 2 匹配且新租约有效,恢复后的本地工作完成。
  8. t=40 A 尝试领取:None;done / generation 2。返回 None。完成后的重复领取不会再执行。
04 · 响应丢失

合成外部服务记下动作,worker 只收到 timeout。 uncertain 停止自动领取,需要外部核对;本模型没有人工裁决接口。

  1. t=0 任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。
  2. t=1 A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。
  3. t=3 外部动作成功,响应丢失:timeout;running / generation 1。这是合成外部流水:动作次数为 1。worker 只观察到 timeout,无法由响应判断成功与否。
  4. t=4 A 提交 uncertain:True;uncertain / generation 1。返回 True。在有效租约内把未知结果落盘为 uncertain,租约清零。
  5. t=5 worker 退出,连接重开:None;uncertain / generation 1。重开后读到 uncertain、generation 1。未知结果由数据库保留。
  6. t=31 B 尝试领取:None;uncertain / generation 1。返回 None。即使旧租约已到期,uncertain 也不在可领取状态中。
  7. t=60 B 尝试领取:None;uncertain / generation 1。返回 None。继续等待不会自动重放;合成外部流水仍为 1,核对与裁决需要另行设计。
05 · 未解决的崩溃窗口

外部动作成功后、uncertain 落盘前,worker 退出。 此模型会允许再次领取:没有外部幂等边界,动作可能重复。

  1. t=0 任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。
  2. t=1 A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。
  3. t=3 外部动作成功,响应丢失:timeout;running / generation 1。这是合成外部流水:动作次数为 1。worker 只观察到 timeout,无法由响应判断成功与否。
  4. t=4 worker 退出,连接重开:None;running / generation 1。A 在写入 uncertain 前崩溃。任务表仍为 running,表里没有外部成功的证据。
  5. t=30 B 尝试领取:None;running / generation 1。返回 None。租约仍保护本地领取,但不提供外部动作的结果证据。
  6. t=31 B 尝试领取:2;running / generation 2。返回 generation 2。模型允许接管;它无法知道上一轮外部动作已成功。
  7. t=32 重放产生第二次合成动作:2;running / generation 2。合成流水变为 2,演示了重复副作用风险。真实动作必须先有稳定 idempotency key、结果查询或核对机制。
  8. t=35 B 提交 done:True;done / generation 2。本地 finish 返回 True,任务表可以成为 done;外部动作却已发生两次。done 本身无法证明恰好执行一次。

把观察带回设计

自动接管只适用于本地可重做的工作,或已经有外部幂等边界的工作。generation 只能保护本地状态,不能撤回外部动作。此模型没有队列 consumer、D1 适配、outbox、退避预算、DLQ 或人工裁决接口。

在仓库根目录运行 python3 examples/job-state/demo.py 可查看原始终端演示。

术语 / FIELD NOTES

在词表中继续阅读