Python 35:关闭服务时哪些任务还能提交结果
回环服务停止接收新连接时,旧连接可能还卡在半行请求上,已经被事件循环接手但尚未进入服务处理函数的迟到 handler 也可能在关闭开始后才运行。直接等 Server.wait_closed() 就算设了排空期限,也可能被旧连接拖住。最初的测试确实失败:它原本要取消一个未完成处理器,却在读超时后才退出,得到 cancelled_handlers=0。失败输出留在 examples/python-language/evidence/35/20261008-shutdown-failed/;修复是先设置关闭门禁并停止接收,再等待已有处理器至指定期限、取消剩余处理器并等它们退出,最后才 wait_closed()。
34 的 JSON 行接口保留每连接一个请求。正常案例中,完整提交请求已回复任务 ID,服务排空没有取消处理器;半行案例设 drain_timeout=0,现有连接收到 EOF、处理器从活动集合移除,新连接不再被接收。补充回归还把 _serve_client 包上一道测试屏障:连接已经进入事件循环,close() 设置 _closing 后才放行 handler。完整提交帧不会新增任务,半包也不会拖住零排空关闭;两者都以连接关闭、活动集合为空收束。实测分别覆盖 cancelled_handlers=0、1 和迟到 handler 的接入门禁。这里的截止时间针对协作中的连接处理器,不是强制抢占 31 中无等待点的 CPU 计算的保证。关闭服务端口也不会把已回复的 queued 任务自动改成完成。
强杀落在哪条事务边界
21 的 SQLite 网关原有的 run 将进入运行态和写入结果放在同一事务里。子进程在事务内发出就绪行,随即被 SIGKILL;没有 Python finally 或正常连接清理的证据。重开数据库时这次未提交的更新已经回滚,任务仍是 queued,可再次正常运行。这不是“未发生过计算或外部副作用”的证明,只说明SQLite 事务没有留下已提交的运行态或结果。
第二个子进程走显式 claim:先提交 running,发出就绪行后等待,父进程将其强杀。重开时看见 running,不是结果。只有在已排除其他活跃工作者的恢复阶段,recover_running() 才把遗留 running 或 cancel_requested 原子地标记为 failed,错误写明结果未知、外部副作用须另查;它不会捏造 cancelled 或成功结果。该实验用声明过的已提交领取点构造可恢复的中断状态,不应把 claim 当成可以独立完成结果提交的生产执行策略。
1 | |
两个被强杀的子进程退出码均为 -9:一个重启后 running → failed,一个事务回滚后仍 queued 并可重跑到 succeeded。本次补跑的回环服务原始输出在 examples/python-language/evidence/completion/core/final-35_shutdown-unrestricted.log,迟到 handler 回归在 shutdown-late-handler-unittest.log;这些测试需要本地 loopback 绑定权限,受限沙箱会返回 Operation not permitted。本章没有把 SQLite 网关装进回环服务器,也没有实施幂等外部操作;事务提交与外部副作用完成是两份不同证据。recover_running() 必须只在确认没有活跃的执行者后使用,否则会把正常正在工作的任务误标失败。
预测题
两个子进程先后被 SIGKILL:A 在 SQLiteTaskGateway.run 的事务内,B 在 claim 已提交 running 之后。重启后分别看到什么?能否推断外部动作恰好执行一次?
答案:A 的未提交事务回滚,状态仍 queued;B 保留 running,独占恢复时标记 failed、结果未知。两者都不能证明外部副作用的次数;重试 A 之前必须先评估工作是否有幂等键或可查询的外部结果。
变更题
将 close 中的 wait_closed() 移回活动连接的排空/取消之前,然后把 drain_timeout 改成零,能保证半包连接马上关闭吗?若恢复程序在其他工作者仍活着时调用 recover_running() 呢?
答案:都不能。第一次局部测试正是先等 wait_closed 而推迟到读超时,修复后先设置关闭门禁、停止接收、终止剩余处理器,再等待服务结束;迟到 handler 在门禁之后只能关闭连接,不能提交新任务。第二种改动会把真实运行中的任务误判为遗留任务。需要先停止接收并确认执行者隔离,再按事务边界恢复;强制结束也不能声称触发了 Python 清理。
系列导航只列已验收文章。





