worker: enforce quotas during execution (claim/retry/fallback/followup/poll kill)

- claim-time gate: queued task whose quota was spent while waiting is
  marked interrupted with a quota reason, no cost row, resume kept
- retry/fallback boundaries and every followup round check DB month
  usage plus this run's accumulated usage (priced via cost table)
- local popen poll loop kills the process group on quota (30s throttle);
  Docker/sync paths get boundary checks only (documented)
- quota stops finalize as interrupted + quota_exceeded error, keep the
  actual cost row, no push, followups restored to pending
This commit is contained in:
host committed 2026-10-27 11:06:40 +08:00
1 parent c189df4a20
commit ad014393ee
3 files changed
+474 -26

No files matched your search

+22 -8
View File
@@ -913,14 +913,28 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
- 准入:建任务、会话分支、重试在权限校验通过后、写库之前查
配额,本月用量 >= 配额时回 402 `quota_exceeded`(附 month、
quota_usd_micros、used_usd_micros),不建任务、不改状态。
老用户与未设配额的用户默认不限额,行为与过去一致。注意
边界:只做准入校验,不做跑中掐断——已在跑的回合、后续消
息触发的新一轮中途超限不会被停掉(worker 侧没有按金额中途
停转的钩子);重试按操作人查配额、费用仍记到原任务创建者
名下(建任务与分支的操作人就是创建者,没有这个错位)。
系统任务(GitHub webhook 的 /fix-review,created_by
是 'system')不记到任何用户名下、不占配额;要把它算到项
目所有者头上需另做归属。
老用户与未设配额的用户默认不限额,行为与过去一致。重试按
操作人查配额、费用仍记到原任务创建者名下(建任务与分支的
操作人就是创建者,没有这个错位)。系统任务(GitHub webhook
的 /fix-review,created_by 是 'system')不记到任何用户名下、
不占配额;要把它算到项目所有者头上需另做归属。
- 执行时加固(worker 侧,`Worker._quota_exceeded`,口径与准入
一致:当月已记账 + 本轮已累计用量,按价目表换算):认领后
开跑前再查一次(排队期间配额被花完/调低时不跑,任务标
interrupted + 配额原因、不记成本、续跑消息不消费);重试与
fallback 换模型前各查一次;后续消息每轮开跑前查一次(超限
时该条回 pending、任务 interrupted,不再排空剩余;建消息时
不查,排队本身不花钱);Local popen 路径在轮询里每 30 秒查
一次,超限时杀进程组(goal 长循环也一样,杀的是整个子进程
组)。配额中断的任务一律 interrupted(可续跑:顶上来后重试
/续跑继续),错误文本带 `quota_exceeded`,已花成本照记
actual,不发终态推送(与取消同款)。诚实边界:Docker/同步
路径没有轮询钩子,只有边界检查,单轮内的超支要到轮次结束
才停;单轮本身花的钱无法中途按 token 流式截断(中转用量只
在回合结束才回传),所以“掐断”粒度是轮次边界 + 长轮询中的
30 秒检查,不是 token 级;并发下两个任务同时查到未超限再同
时开跑会有少量超支(与限流器同款 best-effort,非精确配额);
PR 任务(_run_pr_jobs)暂不查配额。
- 接口:`GET /users/me/quota`(自查,返回 month、quota、
used、remaining,不限额时 quota 与 remaining 为 null);
`GET /admin/quotas?user_id=` 与 `POST /admin/quotas`