后台任务与外部调用
进程崩溃、任务重试之后,该做的事不丢;调用外部服务时不卡死、不刷爆、不重复。
本页内容
任务可能在数据库提交和入队之间丢失,也可能在超时后把外部操作再做一遍;一个慢的服务商还能拖住所有调用它的 worker。导出、通知和履约都要知道哪些工作还没做完,哪些已经做过。
让 AI agent 追踪任务和交付
Prompt
审查 [Jira 编号、PR 链接或 diff] 里的排队任务、重试和外部操作。
如果是 Jira ticket,通过已连接的 Jira MCP 或已认证的 acli 读取描述、验收标准和评论;无法访问时,让我粘贴详情。
把每个操作从工作记录开始,经过队列和外部调用,一路追到完成。检查每个外部调用:有没有显式超时、外面是不是包着数据库事务、怎么重试、有没有守住服务商的限流。用合成数据和测试服务测试重复执行、进程崩溃和超时;涉及资金或奖励时,加上占用和账本检查。
逐条报告重复执行或工作丢失的风险,附上证据。
说明本页列出的测试哪些已在项目测试套件里。能自动化的补上,其余的列出来。
不要重发、退款或修改生产记录。保住工作,并认出重复执行
- 每个操作都有稳定的标识,重复投递时返回已有结果。队列的 attempt ID 每次重试都会变,标识不了业务操作。同一个标识带着不同输入就拒绝,并防止两个 worker 做同一个外部操作。
- 工作记录和需要它的数据变更一起提交,提交后再入队,这样中间崩溃也不会丢活。
- 任务参数要让上一个版本也读得懂。滚动部署时,旧 worker 会接到新任务;回滚之后,新任务会在旧代码上跑。新任务或新参数先发布一个版本,下一个版本再开始排队。
- 外部调用超时后,结果是未知的:用原来的引用号去查结果。对方查不到、也不能去重时,就标为结果未知,留给核对处理,不要新建一个操作。
- 限制重试次数和退避时间。重试耗尽的工作进入可见的失败状态,并指定负责人。
导出 E-42 还是待处理状态,worker 的 attempt J-42 超时了,存储里已有文件 F-42,通知服务商受理了消息 N-42,但还没有投递结果。
- 核对 F-42 和 E-42 的商户、筛选条件和输入版本是否一致,并检查文件内容;文件名对得上不算证据。
- 把文件、完成记录、下载和通知当作四个独立阶段。服务商受理了,只说明它收到了,不说明已经送达。
- 提议只补做缺的那个阶段,生产环境要先经过批准。N-42 标为结果未知并指定负责人;不要因为 J-42 被重试过,就重新生成文件或重发通知。
安全地调用外部服务
- 每个外部调用都显式设置超时,并且不放在数据库事务里。客户端库的默认超时可能是几分钟,甚至没有;放在事务里的调用会一直占着锁,而且事务回滚时撤不回它。
- 连接超时和读取超时分开设,按服务商平时的延迟来定:比如用户在等的调用,连接 1–2 秒、读取 2–5 秒。
- 先记下意图并提交,再调用或把调用放进队列。Rails 里用
after_commit,不要用after_save。Rails。 - 只重试可以安全重复的请求:读请求,或者带了服务商幂等键的写请求。退避时加随机抖动;遇到 429 或 503,按
Retry-After给的时间等。 - 主动守住每个服务商的限流:在所有 worker 和服务器之间统一限速,用共享的限流器,或者固定并发的队列。按进程设的限额,每多一个 worker 就多一份。
- 批量发送、同步和回填放在单独的队列里匀速跑,不能把登录或下单要用的额度用光。
- 服务商挂了,降级的是那一个功能,而不是整个应用:关掉它或者把工作排队,给用户显示处理中,并告警。一个服务商的重试不能把所有功能共用的队列堵满。
- webhook 会重复、会乱序。验证来源之后,先快速响应,再放到任务里处理:响应慢了,服务商会再发一次。记下事件 ID、忽略重复,旧事件绝不能覆盖新状态。
- 生产环境之外用沙箱凭据和测试账号;生产环境的 key 不能出现在 staging。
- 日志里记服务商的请求 ID、结果和我们自己的操作 ID,不记请求内容。结果不明时,靠这些来查清楚。
- 用模拟的服务商,先让它变慢,再让它出错,再让它返回 429。变慢的调用在时限内超时;出错和 429 只按允许的方式重试;没有 worker 卡住,没有重复发送,用户看到的是处理中的状态。
- 用户在发请求时跑一个批量任务。面向用户的调用保持在延迟目标之内,服务商收到的请求从不超过它的限流。
给批量工作和 fan-out 设上限
- 按项目实测的限制,给结果大小、批次大小、并发数和运行时长设上限。分页只限制返回的数据,限制不了查询背后的工作量。
- 耗时的导出放进队列,数据用流式处理,避免一个请求就耗尽内存。任务和文件的访问范围限定在发起请求的用户和租户内。
- 按条目和按任务限制下游调用次数:一个很小的输入也可能 fan-out 成大量查询或外部调用。
- 开销大的接口,比如搜索、导出和报表生成,要在应用里按调用方限流,做法和短信、OTP 与付费接口对付费发送的一样。否则一个调用方就能把它们耗尽,影响所有人。
测试中断和重复的工作
- 并发投递同一个任务,完成后再重放一次:只有一个结果,没有多出来的调用、文件或通知。
- 分别在入队之前、外部服务受理之后、记录完成之前让进程崩溃:能找到待处理的工作,没有任何重复。
- 发送延迟和重复的回调:每个回调都对应到原来的操作,不会重复它的效果,也不会覆盖更新的状态。
- 把重试次数用完:失败记录保留原来的标识、错误和下一步动作。
- 在正常流量中发起允许范围内最大的导出,再发起一个超限的:前者不超出内存和队列限制,后者被拒绝。