为被限流的请求设定有限重试预算

使用 AI 辅助编写与复核。

本文目录

“被限流就重试”会让编程 Agent 自行猜测用户要等多久、应发送多少次请求,以及预算耗尽时屏幕该说什么。在让它加入重试库之前,先给出一份有限日程。

RFC 6585 第 4 节定义了 HTTP 429,并允许响应包含 Retry-After 头;它没有规定通用配额,也没有规定服务器如何统计请求。MDN 的响应头参考介绍了非负整数等待时长和 HTTP 日期两种形式。两者都不承诺下一次请求会成功。

设定一个小范围只读契约

虚构应用 Palette List 读取 GET /palettes?collection=coast。该操作获取列表,不创建渲染作业、不向账户扣费,也不改变已保存的选择。这里所有响应、响应头和时间都是原创教学数据,没有使用网络请求或重试 SDK。

采用以下应用规则:

预算要素 本练习的决定
整个动作的总预算 从第一次尝试开始计算,最多 20 秒
最大尝试次数 3 次,包含第一次
单次尝试上限 4 秒;每次开始前必须预留完整上限
可重试响应 本例仅重试 429
有效等待时长 至少等待服务器要求的时长,同时本地至少等待 1 秒
响应头缺失或非法 本地回退:第 1 次尝试后等 2 秒,第 2 次后等 4 秒
用户离开 停止本动作的等待与后续尝试;不脱离页面继续重试
其他响应或传输失败 按其自身的可见结果停止;不编造另一套重试策略

这些数字是示意性的产品选择,不是 HTTP 要求。真实客户端还需要能执行截止时间的传输超时与取消行为;只计算日程做不到这一点。

预算使用单调递增的已过时间时钟。虚构的日历时钟从 Mon, 01 Jan 2024 12:00:00 GMT 开始,与已过秒数完全同步推进。这个固定对应关系是给定数据,不是真实时钟准确性的测量。

在已过时间 t 收到 429 响应时,计算:

remaining = 20 - t
wait = max(1, parsed server delay)       有效响应头可用时
wait = 2 ** completed_attempts          响应头缺失或非法时
earliest_next_start = t + wait

只有以下条件全部满足,才能开始下一次尝试:
- 动作仍属于当前页面;
- 已启动的尝试少于 3 次;
- earliest_next_start + 4 <= 20。

条件不满足就立即停止。不要为了更早发出请求而缩短有效的服务器等待要求。即使等待本身放得进预算,预留四秒也可能让动作停止;这是有意作出的选择。

日程 A:响应头包含秒数

第一份完整日程依次使用 Retry-After: 3 和 Retry-After: 4。

步骤 已过时间区间 给定数据中的观察 决定
尝试 1 0–2 429;等待 3 秒 下一次在 5 开始;可完整预留至 9
等待 2–5 三秒 没有进行中的请求
尝试 2 5–7 429;等待 4 秒 下一次在 11 开始;可完整预留至 15
等待 7–11 四秒 在同一个动作内继续等待
尝试 3 11–14 200;列表返回 用收到的列表完成动作

虚构日程给定的请求耗时合计为 2 + 2 + 3 = 7 秒,等待合计为 3 + 4 = 7 秒。已过时间为 7 + 7 = 14,不是在 14 之外再加七秒请求时间。四秒上限只是预留,不会自动全部耗用,也不应重复计入。

整数等待时长从收到对应响应时开始计算。如果从 t=0 计算第一次三秒等待,就会错误地在 3 发出下一次请求,而不是 5。

日程 B:响应头包含日期

对于 HTTP 日期,RFC 9110 第 10.2.3 节定义了秒数之外的日期形式。这里用给定日期减去收到响应时的 UTC 时间,把过去日期产生的负值归零,再应用本地一秒下限。将结果一次性转换为基于单调时钟的等待。

步骤 已过时间区间 / 接收时钟 响应头或响应 计算
尝试 1 0–2 / 12:00:02 GMT 429;Mon, 01 Jan 2024 12:00:06 GMT 日期减去接收时间 = 4;下一次在 6 开始
等待 2–6 四秒 下一次可完整预留至 10
尝试 2 6–8 / 12:00:08 GMT 429;Mon, 01 Jan 2024 12:00:12 GMT 日期减去接收时间 = 4;下一次在 12 开始
等待 8–12 四秒 下一次可完整预留至 16
尝试 3 12–14 / 12:00:14 GMT 200;列表返回 完成

请求耗时 6 秒,等待 8 秒,合计 14。之后日历时钟发生调整,不能重置已过时间预算。如果解释日期所需的时钟不可信,本例会以 clock_untrusted 停止,而不猜一个更短的等待时长。教学解析器接受展示的 IMF-fixdate 形式,不是对所有 HTTP-date 形式的完整实现。

写计时器之前,先填好停止案例

所有案例都使用相同策略:20 秒、最多三次尝试、单次上限四秒。完整响应数组与预期合计见 B62-16-schedules.json。

案例 给定数据与计算 要求的结果
缺失响应头 尝试 1:0–2,429;回退等待 2;尝试 2:4–6,200 6 时成功;请求 4 秒 + 等待 2 秒
非法 1.5 与缺失响应头相同;小数不是 delay-seconds 记录非法响应头;使用本地回退;6 时成功
同样非法 -1、tomorrow 或 NaN 使用相同的首次重试回退,不进入无限循环
等待超出预算 t=2 时,429 要求等 30 秒;下一次最早在 32 开始 2 时停止;不缩短等待,不进行第二次尝试
等待放得下,完整尝试放不下 尝试 1:0–4,等待 6;等待区间 4–10;尝试 2:10–14,等待 4;下一次在 18 开始,需要预留到 22 14 时停止;请求 8 + 等待 6;不自动花完最后六秒
次数上限 每次请求 2 秒后得到 429;响应头均为 0;一秒等待使开始时刻为 0、3、6 第 3 次尝试后在 8 停止,尽管预算仍剩十二秒
用户在等待时离开 第一次 429 在 2 到达,要求等 8 秒;用户在 5 离开 一次尝试,请求 2 秒 + 已等待 3 秒;5 时停止,不在 10 尝试
日期形式、时钟不可信 第一次 429 在 2 到达并携带日期,但缺乏可用时钟依据 2 时以 clock_untrusted 停止,不把日期重新解释成零
调度器唤醒过晚 一次原本允许的计时等待唤醒过晚,截止前已放不下预留的四秒 停止;根据当前已过时间重新计算,不沿用之前的判断

每种最终展示都应把最后响应与动作结果分开保留。例如:

已停止:重试预算放不下下一次请求。
最后响应:429。尝试次数:1 / 3。已过时间:2 秒。
服务要求等待 30 秒,没有发出重试。

这并没有宣告服务已恢复或永久失败。可以提供新的用户动作,但它应明确开启一份新预算;旧动作不能悄悄在后台继续。用户在请求进行中离开,也不能证明服务器停止了处理。如果结果之后到达,它仍须属于当前页面/动作,才能更新视图。

区分读取重试与写入恢复

HTTP 将 GET 定义为安全方法,即其预期语义是只读;这不保证所有服务器实现都没有附带效果。参见 RFC 9110 第 9.2.1 节。在采用此模型前,应先确认真实接口是一项读取操作。

配套指南《告诉编程 Agent:重试还是同一个作业,还是另一个作业》为创建操作采用另一份契约:重试保留相同的逻辑操作 ID 与输入;主动要求另一次创建,则使用新 ID。写入响应丢失,可能掩盖已经提交的效果。等待预算不能解决身份与恢复问题。不要把这个读取调度器作为通用重试包装器,套到创建作业上。

复制有限预算任务说明

使用 B62-16-schedules.json,为虚构 Palette List 的 GET 操作建模。
不使用网络客户端、重试 SDK,不访问账户,也不安装依赖。

策略:从首次开始计算 20 秒,合计最多 3 次尝试,
每次开始前预留 4 秒的单次上限。只重试 429。
解析非负整数等待时长,或数据中的 IMF-fixdate 日期。
日期可用时,按固定时钟对应关系与接收响应时的 UTC 时间比较,
再一次性转为基于单调时钟的等待。时钟不可信就停止。
应用本地至少 1 秒的等待。响应头缺失或非法时,
第 1 次尝试后回退为 2 秒,第 2 次后回退为 4 秒。

不能为了放进截止时间而缩短有效的服务器等待要求。
已过请求耗时与实际等待时间各计一次。预留不是已经耗用的时间。
计时器唤醒时,重新检查截止时间、尝试次数和页面归属。
用户离开就取消后续尝试;不脱离页面重试,也不悄悄新建预算。

复现两份完整日程和停止表中的每个分支。
返回每次开始、接收响应、等待、剩余预算、最终原因与可见状态。
最后 HTTP 响应与成功/预算/次数/用户停止结果分开报告。
拒绝耗时超过单次上限的数据;算术模型没有实现真实超时机制。

不要推广到写入。先依据配套请求身份指南,确立创建操作身份、
重试输入不变,以及响应丢失后的恢复契约;这里没有授权写入重试。
把模型执行与未来的传输/浏览器实现分开。

这份日程让 Agent 能具体回答“等待、再试一次,还是停止”。下一项实现决策应是真实传输与界面能否执行相同边界,而不只是某个库是否提供了重试选项。