Python 34:服务接口如何复用既有任务规则
客户端把 {"op":"submit",...} 写入连接并成功 drain(),任务完成了吗?服务可能还没读到换行符,连请求边界都无法确认;即使收到含任务 ID 的提交回复,新任务也仍是 queued。要验证结果,必须再执行 run 并核对返回的业务状态和结果。传输层写入、应用接受和业务完成是三个不同事件。
本篇在 127.0.0.1 的临时端口提供每连接一个请求、每请求一行 UTF-8 JSON 的小接口。submit/run/query/cancel 直接复用已有 app 操作;run 调用 31 的 run_async并在完成后读取任务快照。它没有第二套状态机,也没有允许客户端上传 Python 函数。测试中提交得到 task-0001,立刻查询还是 queued;执行返回 {"one":2,"two":1} 与 succeeded。21 的 SQLite 事务没有被这个内存服务自动接入;重启后的持久化和停机恢复留待 35。
一行也需要边界
服务以 StreamReader.readuntil(b"\n") 收帧,并把接收长度限制为 2048 字节、空闲读取限制为 0.5 秒。测试把完整 JSON 拆成两次写入,第一次尚无换行时 store 没增加任务;后半包和换行到达后才得到提交回复。超过长度的帧返回错误后关闭该连接;缺换行就断连、无效 UTF-8、重复 JSON 字段和未知操作分别拒绝,不应因此多出任务 ID。可用一个连接只提交一个请求,不能借连续多行绕开一连接一请求的约定。
连接入口有两个并行处理槽。两条连接都保持空闲不发换行时,第三条收到 busy;前两条到达读取超时后得到 read timeout 并关闭。这个限制只覆盖进入处理函数的并发连接,不能保证 TCP 内核缓冲、已创建却尚未被调度的连接处理 Task、持久历史任务都在同一内存上限内;33 的入场上限也不能自动替代网络层总容量治理。该服务仅绑定回环地址,不是公网安全服务、无认证、无 TLS。词频计算在事件循环里同步执行,不可据此推断高负载时心跳仍会及时运行。
1 | |
实际输出记录 submit 的任务 ID、query_before_run: queued、run 的词频,以及半包恢复成功、超长帧被拒绝。原始日志和累计 90 项回归见 examples/python-language/evidence/34/20261008-jsonline/。写入成功或仅拿到提交回复都不代表外部副作用完成;即使执行回复已发送,客户端断连也可能使它不知道服务器是否已提交结果。服务需借任务 ID 重查状态,后续再处理停机与重试的幂等边界。
预测题
客户端分两次写 {"op":"submit","kind":"word_count","text":"One two one"}\n,第一次停在 "One 后就断开。服务应该产生任务 ID 吗?完整提交回复收到后又能否判定结果已经计算完?
答案:第一次没有完整帧,不产生任务 ID;完整提交回复只表示新建了 queued 任务,并不包含计算结果。必须另发 run,再以 query 核对已提交状态;连 drain() 成功也不能替代接收回复。
变更题
移除 readuntil 的帧上限,并把每个连接都放到 await queue.put() 等待工作者空闲。仅凭两个工作者的上限,可以保证恶意长行或慢客户端不消耗无界内存吗?
答案:不能。未解帧字节、慢连接和排队的连接 Task 都在工作者之外占资源。应分别设帧长度、读取期限和有界入场策略,并用实际连接测试验证拒绝与关闭;本章的处理槽和小 backlog 也还不是全服务器内存上界。
系列导航只列已验收文章。





