feat: 任务队列限流防护 #220
Reference in New Issue
Block a user
Delete Branch "feat/task-queue-rate-limit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
背景
staging 队列今天两次爆炸(135→595 个 pending),全是批量提交导致的。Worker 处理能力有限(并发1,单任务10+分钟),批量涌入就彻底堵死。
实现3层防护
1. 用户级限流(核心)
您的待处理任务过多,请等待完成后再提交2. 全局限流(兜底)
系统繁忙,请稍后再试3. 双重检查
覆盖的入口
关键设计
>判断语义:入队函数里用>(任务已创建),预检查用>=(任务未创建),边界一致测试
13 个单元测试,覆盖:
全量 1296 个测试通过 ✅
PR #220 审查结论:⚠️ 需要修复 3 个问题(P1×2 + P2×1)
审查范围
任务队列限流防护:用户级 pending 限流(3个上限,429)、全局限流(20个上限,503)。
✅ 做得好的地方
⚠️ P1-1:限流阈值硬编码 + 各入口判断条件不一致
问题:
task_enqueue.py中safe_enqueue_generation_task默认参数user_pending_limit=3, global_pending_limit=20✅generation_tasks.py预检查:硬编码user_pending + count > 3/global_pending + count > 20task_center.py两个重试入口:硬编码user_pending >= 3/global_pending >= 20三个不一致:
safe_enqueue_generation_task用>(超过才拒),task_center.py预检查用>=(等于就拒)。语义不一致会导致边界行为不同pending_count - count来显示当前数,逻辑绕建议修复:
DEFAULT_USER_PENDING_LIMIT = 3/DEFAULT_GLOBAL_PENDING_LIMIT = 20>),保持一致⚠️ P1-2:批量创建的竞态条件
问题:
create_generation_task的预检查在 for 循环外,但实际建任务和入队在循环内。如果两个请求同时到达,预检查都通过了,循环里建了 5 个 + 5 个 = 10 个 pending,远超用户 3 个上限。虽然
safe_enqueue_generation_task里有兜底检查,但有两个问题:建议修复:
user_pending + count > limit,把本次要提交的数量算进去,从源头拒绝看了下代码,预检查确实用了
user_pending + count > 3,这个方向是对的。但有个 bug:> 3意味着 4 个才超限,但用户上限是 3 个。如果用户已有 3 个 pending,再提交 1 个,3 + 1 > 3 = True会被拒绝,这个是对的。但如果用户已有 2 个,再提交 2 个,2 + 2 > 3 = True也会被拒,而如果改成逐个入队的话其实第 1 个是成功的。这个不是 bug,是策略选择——批量提交要么全过要么全拒,比一半成功一半失败好。没问题。
真正的竞态问题:两个并发请求各提交 1 个,用户已有 2 个。预检查都通过(2+1=3 不大于3),然后各自入队,最终用户有 4 个 pending。这个在 DB 层面没有锁。
建议:
safe_enqueue_generation_task里再查一次兜底(但也有竞态,只是窗口小一点)⚠️ P2:预检查逻辑重复
问题:task_center 里的
retry_task_by_id和retry_project_task有几乎一模一样的预检查代码(用户级+全局级),generation_tasks.py 里也有类似的预检查。总共 3 处重复。建议:
enforce_queue_limits(user_id, repo, extra_count=1),超限直接抛 HTTPException总结
P1 问题修复后即可合并。P2 是代码质量优化,可以后续重构。
复审结论 ✅ 通过,可以合并
P1 问题全部修复到位,逐一验证如下:
P1-1 阈值统一管理 + 边界判断统一 ✅
task_enqueue.py顶部常量:USER_PENDING_LIMIT = 3、GLOBAL_PENDING_LIMIT = 20>= limit拒绝> limit拒绝P1-2 并发竞态兜底 ✅
send_task成功后再查一次计数_mark_task_failed_safely()回滚状态为 failed 并抛出异常TestPostEnqueueFinalCheck覆盖了全局/用户超限回滚、优先级、计数不变等场景单测覆盖 ✅
350+ 行单测,分三组:
TestCheckQueueLimits:预检查 8 个用例(正常/用户超限/达到上限/全局超限/优先级/空user_id等)TestSafeEnqueueWithLimits:安全入队 10 个用例(正常/被拒+标记failed/边界值/无user_id/更新失败不崩溃等)TestPostEnqueueFinalCheck:入队后兜底 4 个用例(并发模拟)P2 遗留(不阻塞合并,后续可单独重构)
check_queue_limits_or_429()之类的 helperedit_plans的render_edit_plan任务只有预检查,不走 safe_enqueue(因为是另一类任务,不是 generation_task),如果后续也要兜底需要单独处理整体质量不错,P1 都修干净了,可以 squash merge。