为被限流的请求设定有限重试预算
本文目录
“被限流就重试”会让编程 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 能具体回答“等待、再试一次,还是停止”。下一项实现决策应是真实传输与界面能否执行相同边界,而不只是某个库是否提供了重试选项。