34 的 JSON 行服务用 {"op":"submit"} 表示一次提交。换成 HTTP 时,POST /tasks 可以返回 201,但状态码只表示服务器接受了任务,绝不等于统计已经执行。新增的麻烦是客户端可能在任务执行中断开;如果请求处理器把任务丢到无人管理的后台,query 就可能在连接早已消失后仍看到它悄悄提交结果。

这次选 Starlette 0.46.2 作可移除适配层,由 Uvicorn 0.34.2 提供回环 HTTP 服务,HTTPX 0.28.1 负责实际客户端请求。它们和传递依赖装在仓库外的 /private/tmp/python-language-completion/optional,主线 wheel 仍保持零运行依赖。不会把 HTTP 请求体解释成 Python 模块名、任意函数或文件路径。

映射只发生在边界

experiments/E05_asgi.py 接受的路径固定为 POST /tasks、GET /tasks/{id}、POST /tasks/{id}/run、POST /tasks/{id}/cancel。提交、查询、取消仍分别调用 tasklab.submit/query/cancel;运行沿用已有 run 并放到线程里执行,不另建一套状态机。POST /tasks 返回任务 ID 和 201,随后 GET 为 queued;运行请求的响应才带有 {"one":2,"two":1} 与 succeeded。另一个仍排队的任务接受取消并变成 cancelled,已完成任务再次运行会返回冲突错误。两者的状态含义不同,不能统一标成“已停止”。

读取请求体按块累计,在 4096 字节处拒绝大帧,而不是先把无限请求体一次性 body() 化。拒绝重复 JSON 键、无效常量和非法 UTF-8 返回 400;内容字段虽然 JSON 语法合法,payload 不是字符串时仍交给运行时提交校验并返回 422。非法任务 ID 返回 400,未知任务 ID 返回 404,已经 succeeded 的任务不能重跑。实测拒绝路径包括重复键 400、未知 workload 422、非法 ID 400;这些 HTTP 分类是适配层选定的映射,不是 Python 语言或 HTTP 框架天然保证的业务含义。

谁负责断开的运行任务

运行请求在进入业务执行后等待一个可控 asyncio.Event 屏障,并轮询连接状态。真实 TCP 客户端发出 POST /tasks/{id}/run,等待服务端确认任务已经进入 running 后再关闭 socket,随后服务端观察到断连并走协作取消分支;本次复跑的 cleanup_count 为 1,任务终态为 cancelled,tracked_tasks_remaining 为 0,服务关闭也返回完成;没有把断开的请求偷偷提交为成功结果。这里使用 499 作为本地适配层内部记录,这个非标准状态码不能声称断开的客户端收到了它。反过来,正常请求返回 200 后结果保持 succeeded;稍后的断开无权把已提交结果改写为取消。

1
2
3
http.request → submit → queued → run 检查点通过 ── 词频结果提交 → succeeded
↘ running 检查点发现 TCP 断开
http.disconnect ─→ 协作取消并等待清理 ─→ cancelled,未提交结果

应用的 lifespan 退出会向适配层持有的后台 worker 发出停止信号,并用 shield 等待真实 worker 结束,但不能把进程被 SIGKILL 当成执行过清理;35 的进程恢复仍有单独规则。当前适配层只使用内存仓储,HTTP 200 已形成任务状态并不证明外部邮件或其他副作用完成;断连测试只覆盖业务执行中的受控检查点,不承诺阻塞中的任意 CPU 函数可被协程取消。

可重放的证据与练习

从仓库外执行 experiments/E05_verify.py,Uvicorn 绑定 127.0.0.1 的随机端口,HTTPX 真正访问提交、执行、查询、取消接口;断连分支使用真实 TCP socket 发请求后关闭连接,并用事件屏障保证服务端已进入处理器。examples/python-language/evidence/completion/http/E05_verify.json 保存本次解释器路径、HTTP 响应、错误输入和断连清理结果。这不是公网部署,也不是 HTTP 服务的压力测试。

预测题。 服务器返回 201 后立刻 GET,再向同一任务发 cancel,能否因为得到 201 就推断任务已执行?答案:不能;submit 只创建 queued。如果还没有调用 run,取消响应里的状态是 cancelled,结果仍为 null。

变更题。 先让运行检查点通过、观察 succeeded 响应,再在客户端触发断开,任务能变回 cancelled 吗?答案:不能,结果已经提交;定向实验同时断言 HTTP 状态 200、最终任务状态 succeeded。若在任务已经进入 running 的检查点断开,本次实验观察到清理分支计数加一且任务变为 cancelled;不能凭随机睡眠猜竞态胜者。

参考资料

  • ASGI Lifespan 规范:启动和关闭作用域;它不替应用自动回滚外部副作用。ASGI HTTP 规范:http.disconnect 消息与响应时机。
  • Starlette Lifespan、Uvicorn 部署指南:框架及服务器职责;具体行为以冻结包及本地事件实验为准。
  • examples/python-language/experiments/E05_asgi.py、examples/python-language/experiments/E05_verify.py 与 examples/python-language/evidence/completion/http/E05_verify.json:完整映射、正反例、日志和版本。