两个工作者都在等待一扇没打开的事件门。如果只限制工作者数量,生产者仍可以无限次 queue.put(),请求在排队时照样占内存。即使队列有上限,服务入口若给每个请求创建一个等待队列空位的 Task,等待入场的 Task 本身也没有上限。

32 的任务组约束故障和取消后子任务如何退出;本篇先限制可接收但尚未完成的数量。AsyncTaskQueue 配两个工作者和一个排队槽,入口同时检查 admitted < workers + waiting 与 Queue.full(),用 put_nowait() 入队。无空位时同步抛 BusyError,不创建任务 ID,也不在后台制造另一个等待队列。校验失败的输入仍由已有 app.submit 拒绝,不消耗容量。

谁占用容量

队列里的 queued 工作与工作者里 running 的工作都占一个 admitted 配额。测试用 Event 把两个工作者停在 run_async 的等待点,再递交第三项:快照为 running=2, queued=1, admitted=3, capacity=3。第四次提交立即被拒绝,store 中实际只有三个任务 ID;这同时验证“执行上限”和“接收上限”,不以仅有两个运行中的 Task 冒充全链路背压。

取消队列中的第三项后,业务状态立刻是 cancelled,但它仍占排队槽和入场配额,直到消费者取出并调用 task_done()。这防止在任务尚未从队列出列时重复发放容量。打开门后,前两项按原词频合同得到 succeeded,第三项不执行也没有结果;queue.join() 返回时 admitted=0、running=0。随后第四个新任务成功接收、完成。close() 先停止接收,再等现有项排空,最后令工作者退出;本篇仅测试正常排空,不声称故障停机时一定能及时退出。

1
2
3
cd examples/python-language
PYTHONPATH=src /usr/bin/python3.12 experiments/33_backpressure.py
PYTHONPATH=src /usr/bin/python3.12 -m unittest discover -s tests -v

实测慢消费者时 2 + 1 = 3、第四次 rejected=true、任务总数为 3;恢复后状态依次为 succeeded, succeeded, cancelled, succeeded。命令、原始输出和累计 87 项测试保存在 examples/python-language/evidence/33/20261008-backpressure/。这里限制的是这个事件循环里单个队列同时未完成的接收量:已完成的任务仍保留在内存 store 中,多轮运行会持续增长;没有网络连接与请求解析层,也没有跨线程安全承诺。不能把这三个容量计数写成整台服务的内存上限。下一篇才在回环接口边界限制连接和帧大小。

预测题

两个工作者已开始且都等待事件,排队槽里有第三项。调用 cancel(store, third) 后,第三项的业务状态、queued 与 admitted 立即是什么?

答案:业务状态 cancelled;队列仍有一个槽位被占用,queued=1、admitted=3。只有消费者取出这项、做完队列记账后,两个计数才下降。状态变化不是容量已释放的证据。

变更题

移除入场的 admitted 检查,改成给每次提交都创建一个 Task,在里面 await queue.put(...)。队列本身依然只容纳一项,还能据此保证同一时刻的内存占用受限吗?

答案:不能。队列槽位虽受限,等待空位的提交 Task 与捕获的输入仍可能随请求数增长。若要允许等待,还需对等待入场的人数及其生命周期另设上限;本章采用明确拒绝,避免在这个入口累积无界等待者。网络层的接收上限和持久化结果保留策略是另外两项独立约束。

系列导航只列已验收文章。

参考资料