Go 39:空环境如何验收任务完整生命周期
38 章把累计任务服务做成可构建的进程。这还不是验收:提交响应、数据库记录和进程退出各自只能回答一个问题。本章从空目录和隔离 PostgreSQL 16.15 启动,把故障注入到同一个服务的入口、领取和终态提交点。完整工程与原始实验包、SHA-256、独立复跑步骤随文交付。
先定位要验证的承诺
源码冻结在 Go 1.27.1、lib/pq v1.12.3。serve.go:runTaskServer 只负责配置、监听和关闭;任务创建与查询仍在 http_tasks.go:create/lookup,SQL 条件领取与终态提交仍在 sql_tasks.go:Claim/Finish。包内 evidence/39/ 保存初版实验的源码哈希、符号位置和二进制版本;补齐后的当前源码由 CURRENT-SHA256SUMS 校验,不能用旧日志的哈希代替。capture39.sh 初始化全新 PostgreSQL 实例,go test/race 回归、构建程序,然后在专属 go39live 库运行 verification/39.sh。包外新目录还要重做一遍,不能将上一次 go test 输出当解包验收。
HTTP 202 表示已接纳,随后要按响应中的 ID 查询最终状态;查询到 SUCCEEDED 再核对 SQL 中该 ID 的 words/bytes/runes,才有本次统计任务的结果证据。样本 {"text":"Go one"} 在此次隔离库返回 task[0],GET 到 SUCCEEDED,SQL 行 task[0]|SUCCEEDED|2|6|6。这不是在所有数据库中固定从零开始的 ID:脚本解析实际 POST 响应,不按假定 ID 查询。收到 202 与得到统计结果之间存在窗口。
故障矩阵里的正反结果
下表是此次真实二进制和 PostgreSQL 的路径;每条对应 evidence/39/ 里的命令、双流、退出码和响应体。脚本对状态与 SQL 行执行断言,而不只打印预期文字。
| 输入或闸门 | HTTP 与任务 ID | 数据库、退出或反证 |
|---|---|---|
--admin 0.0.0.0:0、非法 JSON、未知 ID |
启动退出 2 且无监听地址;400、404 | 配置在监听前拒绝;malformed.body、missing.body |
Go one 后查询;四词超过上限 |
接纳 202,分别查到 SUCCEEDED 与 FAILED |
accepted-sql.stdout 的结果是 2 词;failed-sql.stdout 的 words 为 0;接纳不等于成功 |
| 延迟 SQL 领取,单 worker/单排队位再投第三条 | 前两条 202,第三条 503 | rejected-sql.stdout 行数为 0;拒绝任务不留可查询 SQL 副本 |
| 上述排队任务取消两次 | 首次 202,轮询到 CANCELED;再次 409 |
取消的队列记录没有执行统计;已完成的任务取消也返回 409 |
| PostgreSQL 停止、恢复 | GET/POST 均 503,重启后原 ID GET 200 | 拒绝期间的 db lost 在恢复后行数为 0;故障不变成虚假的 202 |
SIGTERM、重启同一数据库 |
退出 0、旧 ID GET 200/SUCCEEDED |
两个监听端口可重新绑定;Close 等待业务 worker,Shutdown 本身不代替这个等待 |
延迟终态提交、读到 RUNNING 后 SIGKILL |
进程 wait 137;再启动后旧 ID FAILED |
crash-sql.stdout 的 words=0,原因是 process exited before worker completion,不谎称外部工作成功 |
前一组触发器只延迟 SQL 领取,冻结运行中 worker,制造确定的过载与排队取消;后一组延迟 SUCCEEDED 更新,让杀进程时数据库仍能读到 RUNNING。测试专属 SQL 在 verification/39-claim.sql 和 39-finish.sql,实测完删除触发器。pg-fail 与 pg-recover 的原始输出分别证明数据库真实停止和恢复,并非 mock。sql-final.stdout 列出本轮所有任务 ID 和终态。
领取报错还需要单独检验。202 返回后,SQL 的 QUEUED→RUNNING 更新可能失败;旧执行器把错误送回内部结果通道,SQL 行仍停在 QUEUED。新增 TestSQLClaimFailurePersistsTerminalWithoutAnalysis 在旧实现实际失败,确认这个断言能识别悬挂。当前实现不重新领取、不启动分析,只条件更新仍为 QUEUED 的行:记录固定原因 worker could not claim task、FAILED 和三个零统计字段。终态写入失败时保留 worker 槽位,25–250 毫秒退避,每次 SQL 限时两秒,直到提交成功或任务/执行器取消;Close 等待 worker,Shutdown 或父 context 取消停止重试。新增真实 PostgreSQL 测试还验证写入先失败再恢复、取消和停机退出、既有状态不被覆盖。
领取事务返回连接错误不证明它没有提交。如果数据库实际已是 RUNNING,补偿更新不会覆盖它,也不会重跑分析;旧实例退出后按单实例恢复处理。数据库中已有终态同样保持原值。上述修复和 2026-10-12 的独立复跑记录分别位于仓库 examples/go-language/、writing-plans/go-language/completion/;表内初版实验与新记录分开保存。
取消请求不等于 worker 退出。真实进程中的排队任务可以立刻转为 CANCELED,但不能由此推断运行中的分析器已经停止;同一隔离 PostgreSQL 上的受控 TestSQLHTTPRunningCancelWaitsForAnalysisExit 固定闸门:取消返回 202 时 SQL 仍为 RUNNING/cancel_requested=true,释放分析器并等待服务关闭后才是 CANCELED/words=0。matrix-gates.* 还单独运行 SQL 条件领取竞争、事务失败回滚、连接池超时、关闭与异常恢复的测试;这些结果不冒充上述真实进程的同一次 HTTP 会话。
进程崩溃会留下一个容易忽略的窗口:首次试跑在 SIGKILL 后立刻重启,PostgreSQL 后端仍在 pg_sleep 并占用更新锁,启动迁移因超时失败。原始失败证据留在 pilot-restart-sleep/。复跑时明确记录 release-crashed-connection.*,终止这个故障注入专用连接,再重启并核对旧 ID 的 FAILED 与原因。这只验证了带受控连接释放的恢复路径,不能推断任意时刻中断后都能在固定秒数内启动。
SQL 提交不等于外部副作用。服务只做文本统计,没有发送邮件或执行第三方付款,也没有给外部动作实现去重;SUCCEEDED 只能表示本服务规定的统计结果已持久化。HTTP 提交应答丢失后客户端是否重试、外部动作的结果未知窗口,均不属于此轮通过范围。没有 race 报告只意味着这些被执行的路径未报竞争;远端部署、其他平台、TLS、全站发布均为 NOT_RUN。主线 00–39 到这里可以单独核算,但八篇选修未完成前,48 篇全系列不算完成。
练习:①看到 202 但 GET 尚为 RUNNING,能否报告任务成功?答:不能;应拿实际 ID 等终态,再用 SQL 结果核账。②取消接口返回 202 就能释放 worker 槽位吗?答:不能;用闸门区分 cancel_requested 与分析器退出,等待服务 Close 或相应完成信号。③进程杀死时数据库还是 RUNNING,重启为何不重试执行?答:单实例恢复将遗留记录标为带原因的 FAILED;若自动重试且外部动作已发生,就可能重复副作用,多实例租约语义留待 E08。
参考资料
- Go 1.27.1
net/http.Server.Shutdown与context.CancelFunc:固定工具链的go doc原文在evidence/39/doc-http.*、doc-context.*。 - PostgreSQL 16
CREATE TRIGGER:本机psql \h CREATE TRIGGER原文和真实触发器 SQL/正反观察都在素材包中。






