Skip to content

事件、取消与任务恢复 ​

前置为第一章与 Python 字典。用户关闭浏览器时,生成请求可能结束,也可能仍在服务器执行。把连接状态当作任务状态,会造成“页面显示取消,订单仍然提交”的矛盾。需要给任务一个独立 ID,通过持久化状态判断进展。

状态转移比布尔值明确 ​

queued → running → succeeded 是成功路径;running 还可以进入 failed 或 cancelled。终态不允许被迟到事件改回成功。下面的纯函数是事件投影器:读取旧状态和新事件,产生新状态;它本身不调用模型。

python
TRANSITIONS = {
    ('queued', 'start'): 'running',
    ('queued', 'cancel'): 'cancelled',
    ('running', 'finish'): 'succeeded',
    ('running', 'error'): 'failed',
    ('running', 'cancel'): 'cancelled',
}

def apply(state, event):
    if event['seq'] <= state['seq']:
        return state
    if event['seq'] != state['seq'] + 1:
        raise ValueError('event gap')
    target = TRANSITIONS.get((state['status'], event['type']))
    if target is None:
        raise ValueError('invalid transition')
    return {'status': target, 'seq': event['seq']}

state = {'status': 'queued', 'seq': 0}
for event in [{'seq': 1, 'type': 'start'}, {'seq': 2, 'type': 'cancel'}]:
    state = apply(state, event)
assert state == {'status': 'cancelled', 'seq': 2}
assert apply(state, {'seq': 2, 'type': 'cancel'}) is state
try:
    apply(state, {'seq': 3, 'type': 'finish'})
except ValueError as error:
    assert str(error) == 'invalid transition'
else:
    raise AssertionError('late success published')
print('cancel wins')

保存为 events.py,输出 cancel wins。先接受的取消使状态终结;随后 finish 被拒绝。若 finish 先被事务提交,取消也应该被拒绝并返回已经完成。这里没有声称撤销已经发生的副作用。

从纯函数到持久化 ​

多个执行者都可能从 seq=1 读到 running。写入时使用比较并交换:只有数据库仍处于 seq=1 才能提交 seq=2,另一方必须重新读取。这把竞态转化为可发现的冲突。内存字典没有跨进程保证,生产实现需事务和唯一事件序号,不能仅复制上面的循环。

恢复时从快照的 seq 开始重放更晚事件。事件相同 seq 但载荷不同属于数据损坏,应由存储的事件 ID/哈希检查,而不能用“序号旧了”静默掩盖。模型调用也不能随每次投影重放:重放只恢复状态,新的调度决定才允许新调用。

流式文本是展示增量,终态事件是结果协议。收到若干 token 后连接中断,只能标记连接断开;客户端用任务 ID 查询服务器状态。断线续传携带已确认序号,服务端返回之后的事件;缺口要补拉,不能把未知间隔当作成功。

练习 ​

增加 queued 状态的 reject → failed 事件,用完整轨迹检验拒绝后不能 start;检查 seq 从 0 直接跳到 2 必须报 event gap。再解释为何 UI 的取消按钮禁用不能取代服务端的终态守卫。

本章答案 · 下一篇:副作用

用心记录每一步成长