异步任务的协作推进
异步功能进入产品讨论后,“取消”往往最先产生误解。界面上的取消按钮可能只是停止等待,也可能要求后台真正终止;如果任务已经把请求交给外部服务,甚至可能只能丢弃迟到结果。三个行为对用户看起来都叫取消,研发实现却完全不同。
我会在写代码前把任务状态画出来:尚未开始、正在处理、已提交外部操作、完成、失败、取消中和已取消。产品确认每个状态的文案与可用按钮,研发说明哪些转换可以保证,测试再据此准备场景。这样讨论的是具体状态,不是“最好能马上停”之类无法验收的描述。
取消信号不是回滚指令
在 Tokio 里,可以用select!同时等待工作完成和取消信号:
tokio::select! { _ = work() => {}, _ = cancel.changed() => {} }这段示例只表达“停止等待哪一条分支”,没有证明work()已经撤销副作用。如果work()内部写入数据库、发送消息或调用第三方接口,Future 被丢弃后,外部操作仍可能完成。产品文案因此不能笼统承诺“操作已撤回”,研发也不能把 Future 结束当作事务回滚。
对于只读计算,取消后释放内存和子任务通常就够了。对于写操作,则要设计幂等键、状态查询或补偿流程。若系统无法可靠撤回,就明确显示“已停止等待,结果可能稍后完成”,并决定迟到结果是入库、忽略还是交给人工处理。
协作接口里要有任务身份
前端、后端和执行器需要用同一个任务标识关联状态。创建请求返回任务 ID,查询和取消都针对该 ID;重复点击取消应得到稳定结果,而不是每次触发新的操作。错误返回也要区分“任务不存在”“已经完成”“取消已受理”和“当前阶段不能取消”,否则界面只能统一显示失败。
任务状态由哪一层负责同样要写清。前端只展示服务端确认过的状态,不能因为本地请求中断就自行标记已取消。后端若把任务放入队列,需要说明排队阶段能否移除;执行器开始处理后,又由谁传播取消信号和清理资源。跨进程取消很难做到瞬时一致,因此状态允许短暂过渡,但最终结果必须可查询。
用延迟任务验收最麻烦的分支
联调不必依赖真实外部服务,可以用受控的延迟任务分别停在排队、执行和结果返回前。测试重复取消、完成与取消同时发生、客户端断开后重新进入页面,以及服务重启后的状态恢复。检查范围包括界面提示、服务端记录、子任务退出和临时资源清理。
如果完成和取消同时到达,需要预先规定谁优先,或者允许接口返回最终已完成状态。不要让不同组件各自猜测。日志只记录任务随机标识、阶段和状态变化,不写用户提交的正文。出现争议时,时间线能够说明信号到达了哪一层。
异步协作推进得顺不顺,关键不在于用了哪种 channel,而在于状态语义是否一致。把取消、迟到结果和重复请求这些麻烦情况先说透,主流程反而更容易实现,也不会等上线后再靠文案掩盖技术边界。