OFFLINE LAB / 合成场景
任务停了,
下一步由谁接手?
拨动模拟时钟,观察租约、generation 与未知结果如何决定下一次操作。
每一步来自构建时实际执行的 SQLite Jobs 模型。浏览器回放记录;没有网络调用、真实外部动作或 Cloudflare 实测。
模拟时钟
任务表 / synthetic-jobpending
generation 0
本地租约
无有效租约
合成外部流水0
已记录的动作次数
- pending登记
- running持有租约
- done完成
- uncertain停下核对
事件时间轴
点选节点可直接跳到该步。
五条真实执行路径
交互回放需要 JavaScript;以下步骤在构建时由同一模型执行,始终可阅读。
01 · 正常完成
登记、领取、完成各有自己的状态。 done 之后重复领取仍被拒绝。
t=0任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。t=1A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。t=8A 提交 done:True;done / generation 1。返回 True。generation 匹配且 t=8 < 31,结果被接受,租约清零。t=10B 尝试领取:None;done / generation 1。返回 None。done 不在可领取状态中,重复到达不会再执行。
02 · 中断与接管
worker A 退出,数据库留住了租约。 租约到期允许接管;到期并不证明旧 worker 已停止。
t=0任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。t=1A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。t=4worker 退出,连接重开:None;running / generation 1。合成 worker A 中断;关闭后重开同一个 SQLite 文件。running、generation 1 与租约都还在。t=30B 尝试领取:None;running / generation 1。返回 None。t=30 < 31,仍在租约内;数据库拒绝再次领取。t=31B 尝试领取:2;running / generation 2。返回 generation 2。边界 t=31 满足 lease_until <= now,B 接管并获得截止 t=61 的新租约。t=35B 提交 done:True;done / generation 2。返回 True。generation 2 匹配且新租约有效,恢复后的本地工作完成。t=40A 尝试领取:None;done / generation 2。返回 None。完成后的重复领取不会再执行。
03 · 迟到的旧结果
新 generation 接管后,旧 worker 回来提交。 generation 保护本地提交;旧持有者不能覆盖新一轮结果。
t=0任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。t=1A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。t=4worker 退出,连接重开:None;running / generation 1。合成 worker A 中断;关闭后重开同一个 SQLite 文件。running、generation 1 与租约都还在。t=30B 尝试领取:None;running / generation 1。返回 None。t=30 < 31,仍在租约内;数据库拒绝再次领取。t=31B 尝试领取:2;running / generation 2。返回 generation 2。边界 t=31 满足 lease_until <= now,B 接管并获得截止 t=61 的新租约。t=32A 提交 done:False;running / generation 2。返回 False。旧 generation 1 与当前 2 不匹配;状态仍为 running,B 的结果没有被覆盖。t=35B 提交 done:True;done / generation 2。返回 True。generation 2 匹配且新租约有效,恢复后的本地工作完成。t=40A 尝试领取:None;done / generation 2。返回 None。完成后的重复领取不会再执行。
04 · 响应丢失
合成外部服务记下动作,worker 只收到 timeout。 uncertain 停止自动领取,需要外部核对;本模型没有人工裁决接口。
t=0任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。t=1A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。t=3外部动作成功,响应丢失:timeout;running / generation 1。这是合成外部流水:动作次数为 1。worker 只观察到 timeout,无法由响应判断成功与否。t=4A 提交 uncertain:True;uncertain / generation 1。返回 True。在有效租约内把未知结果落盘为 uncertain,租约清零。t=5worker 退出,连接重开:None;uncertain / generation 1。重开后读到 uncertain、generation 1。未知结果由数据库保留。t=31B 尝试领取:None;uncertain / generation 1。返回 None。即使旧租约已到期,uncertain 也不在可领取状态中。t=60B 尝试领取:None;uncertain / generation 1。返回 None。继续等待不会自动重放;合成外部流水仍为 1,核对与裁决需要另行设计。
05 · 未解决的崩溃窗口
外部动作成功后、uncertain 落盘前,worker 退出。 此模型会允许再次领取:没有外部幂等边界,动作可能重复。
t=0任务已登记:None;pending / generation 0。任务表为 pending、generation 0。登记只确认接收,尚未完成。t=1A 尝试领取:1;running / generation 1。返回 generation 1;原子领取把状态改为 running,租约截止 t=31。t=3外部动作成功,响应丢失:timeout;running / generation 1。这是合成外部流水:动作次数为 1。worker 只观察到 timeout,无法由响应判断成功与否。t=4worker 退出,连接重开:None;running / generation 1。A 在写入 uncertain 前崩溃。任务表仍为 running,表里没有外部成功的证据。t=30B 尝试领取:None;running / generation 1。返回 None。租约仍保护本地领取,但不提供外部动作的结果证据。t=31B 尝试领取:2;running / generation 2。返回 generation 2。模型允许接管;它无法知道上一轮外部动作已成功。t=32重放产生第二次合成动作:2;running / generation 2。合成流水变为 2,演示了重复副作用风险。真实动作必须先有稳定 idempotency key、结果查询或核对机制。t=35B 提交 done:True;done / generation 2。本地 finish 返回 True,任务表可以成为 done;外部动作却已发生两次。done 本身无法证明恰好执行一次。
把观察带回设计
自动接管只适用于本地可重做的工作,或已经有外部幂等边界的工作。generation 只能保护本地状态,不能撤回外部动作。此模型没有队列 consumer、D1 适配、outbox、退避预算、DLQ 或人工裁决接口。
在仓库根目录运行 python3 examples/job-state/demo.py 可查看原始终端演示。