AI SaaS 工程诊断实战(八)
生成按钮先后触发 A、B 两次请求,B 先回来,A 后回来。最后显示 A,接口日志却全部成功。
这种问题不必先归因于缓存或模型。只要回调拿到结果就写当前页面,完成顺序就可能替用户决定最后展示什么。
本文用本地构造程序重放回调顺序,没有真实网络、模型调用或客户数据。AI 辅助整理,程序已在 Python 3.12 执行;示例是状态机演示,不是生产并发压测。
先区分请求成功与结果仍然有效
A 成功,只能说明 A 对应的一次计算拿到结果。如果用户已经用新输入启动 B,A 可能已经不适用于当前展示。
最小错误逻辑是:任何完成回调都把 status 改为 done,把 text 替换为返回文本。B、A 先后执行时,A 就成为最后写入者。
加等待时间没有解决资格判断。禁用按钮能限制一部分入口,但输入变化、组件切换和取消后的回调仍需要明确处理。
把当前代次带到回调里
本例维护一个 version。每次开始时递增,回调保存自己的 token;接收时要求 token 等于当前 version,而且状态仍为 running。
开始A:version=1,A保存token=1
开始B:version=2,B保存token=2
B完成:2==2,接收
A完成:1!=2,忽略
本地取消也递增 version,立即让在途旧 token 失效。随后可以显示 cancelled,旧回调不能再把它改回 done。
这不是终止计算。服务端可能继续运行,资源和费用也可能继续发生。只有真实调用了相应取消机制并确认结果,才能描述成服务端任务已停止。
完整可运行示例
保存为 late_result.py,运行 python late_result.py。只有标准库,不发送网络请求。程序主动安排回调次序,避免依靠 sleep 凑出偶发失败。
import json
def replay(guarded):
state = {'version': 0, 'status': 'idle', 'text': None}
def start():
state['version'] += 1
state.update(status='running', text=None)
return state['version']
def finish(token, text):
if guarded and (token != state['version'] or state['status'] != 'running'):
return 'ignored'
state.update(status='done', text=text)
return 'accepted'
a = start()
b = start()
b_result = finish(b, 'new result B')
a_result = finish(a, 'old result A')
overlap = dict(state)
c = start()
state['version'] += 1 # Invalidate the active attempt on local cancel.
state.update(status='cancelled', text=None)
cancelled_result = finish(c, 'late result C')
after_cancel = dict(state)
d = start()
finish(d, 'fresh result D')
return dict(overlap=overlap, after_cancel=after_cancel, final=dict(state),
callbacks=dict(b=b_result, a=a_result, c=cancelled_result))
if __name__ == '__main__':
broken, guarded = replay(False), replay(True)
assert broken['overlap']['text'] == 'old result A'
assert guarded['overlap']['text'] == 'new result B'
assert broken['after_cancel']['status'] == 'done'
assert guarded['after_cancel']['status'] == 'cancelled'
assert guarded['final']['text'] == 'fresh result D'
print(json.dumps(dict(broken=broken, guarded=guarded), indent=2))
实际结果:故障版重叠任务显示 old result A;保护版显示 new result B。取消路径中,故障版被 C 改回 done,保护版保持 cancelled。新一轮 D 在保护版里仍能正常完成。
为什么同时检查状态
代次限制了响应属于哪一轮,running 限制了当前是否仍在接收完成结果。它们共同表达本例的接收条件。
如果产品允许“已完成后刷新部分结果”,规则就不能直接照搬,需要先定义哪些状态允许刷新、怎样区分同一任务内的更新。不要为了让样例通过把真实状态规则删掉。
本例只有一个展示槽。多条任务并列时,每个任务需要自己的标识与代次;独立对象不能被同一个全局 version 错误作废。
真实前端还要逐个查回调入口
成功、失败和进度都可能改状态。只保护成功回调,旧失败仍能覆盖当前错误提示,旧进度仍能让已完成的任务退回百分比状态。
组件卸载、路由切换、多窗口和后台恢复也有不同的生命周期。本文没有实际运行浏览器或服务端任务,不能把这个状态机断言写成全链路验证。
接业务写入时,更要区分“页面忽略”和“服务端没有副作用”。如果 A 已经写了业务表,页面忽略 A 并不能撤销写入。需要服务端按业务版本或任务状态检查写入资格,再做相应回归。
我会把验收条件写成明确顺序:启动 A、启动 B、接收 B、再接收 A,观察仍为 B;取消 C 后接收 C,观察仍为取消。这样的记录比一句“异步问题已修复”更容易复查。
—— Linksass|AI SaaS 独立开发者