Go 23:请求取消与任务取消为何分别建模
上篇用 POST /tasks 返回 202 和任务 ID,后台执行不占住提交请求。本章要解决另一半:客户端放弃一次 HTTP 请求、用户主动要求取消任务、服务器停止接收请求,是否都会让那个任务退出?答案只能由实际 context 所属者、记录提交点和 worker 的退出证据给出。在同一个累计工程里增加按 ID 取消接口,保留原 worker、队列和存储;完整工程包、SHA-256与解包复跑保留测试原始结果。
一条任务不能借用短命的请求 context
固定 Go 1.27.1 的 Request.Context 文档规定:对入站请求,客户端连接关闭、HTTP/2 请求取消,或 handler 返回,都可能取消该 context。POST /tasks 的 handler 本来就要在任务完成之前回 202。若把 request.Context() 存进队列,正常应答结束也会变成任务取消信号。这个 context 只属于本次请求的解析与应答,不属于已经被服务接受的业务任务。
现在每次接受任务都从执行器 context派生一个逐任务 context,并保留相应 CancelFunc;请求 context 不传给 worker。队列项带着文本、位置和服务拥有的 context 进入原执行器,worker 执行时使用这份任务 context。任务自然完成时取消函数仍会被调用,以释放该子 context 的关联资源;服务回收对已完成任务的取消函数引用。拒绝入队则立刻移除未被接受的记录并调用 cancel。整个服务仍是进程内内存模型;这不是“客户端断开以后数据已经持久化”的保证。
TestHTTPClientCancelAndServerShutdownDoNotWaitForBusiness 真正发出一次本地 loopback POST,读完 202 后取消客户端持有的请求 context。分析器被闸门挡住,此时 GET 仍查到 RUNNING、CancelRequested=false。这证明本次已接受任务没有因为这个客户端取消而退出;并不证明客户端在完全没拿到响应、网络结果未知时能知道它是否已被接受。后一种窗口需要明确的查询键/幂等协议,不能从一次已收到 202 的测试推出来。
主动取消先登记意图,再等分析器真的停
新增 POST /tasks/{id}/cancel。找不到 ID 回 404;任务已终态或同一取消请求已登记,回 409。首次请求由 TaskStore.Cancel 锁住记录做状态判定,并调用该任务的取消函数。若任务尚在 QUEUED,从未开始计算,记录可立即提交 CANCELED;日后 worker 取到该项时不会再成功 Start。若任务正在 RUNNING,记录先保持 RUNNING、设置 cancel_requested=true,响应仍为 202。取消 API 的 202 只确认取消请求已登记,不保证计算退出。
运行中分析器可能理会 context,也可能暂时无法理会。原 taskExecutor.process 从分析器返回后才观察任务 context 和 CancelRequested,在原来的状态锁提交点调用 Complete 或 Fail:若取消已占先,终态才变成 CANCELED,计数和业务结果不被错误写成成功。若计算结果已先提交为 SUCCEEDED/FAILED,后来取消只能得到 409,不能把终态反转。一次 RUNNING 取消是否最终到终态,仍取决于实际工作能否走到返回点;context 不是抢占式中断按钮。
单独的闸门实验让分析器故意忽略已取消的 context:主动取消 POST 得到 202 后,GET 仍是 RUNNING、cancel_requested=true、终态计数 0;重复取消 409,缺失任务 404。只有放行闸门、统计函数返回,并等待执行器收尾后,GET 才是 CANCELED,词数没有写入,终态计数 1。与之对照,另一个测试用唯一 worker 阻住第一项,第二项排队时取消:第二项立即 CANCELED,统计函数没有为它执行;放行第一项并优雅 Close 后两项各有终态。这分别给出“排队尚未开始”与“正在执行须等退出”的证据,不能把两者压成同一个取消瞬间。
HTTP 服务器停止,不替业务执行器等待
固定版 http.Server.Shutdown 公开契约说它关闭监听、关闭空闲连接并等待活动 HTTP 连接回到空闲;该 API 并不知道本工程单独创建的任务执行器。先前拿到 202 的 POST 已经结束,业务却还在另一个 goroutine 内运行。上述 loopback 测试随后真实调用 httptest.Server.Config.Shutdown:它返回时分析器仍停在闸门、任务仍为 RUNNING;另起 goroutine 调用业务执行器的优雅 service.Close(),在闸门放行前它不会返回。放行后计算成功,业务 Close 才结束。这里没有把真实 HTTP Shutdown 的完成误称为全部 worker 退出,也没有把 service.Close 说成会自动关闭 HTTP 监听端口。
这个对照只覆盖普通请求连接。Server.Shutdown 文档明确对 hijacked 长连接不负责,它们需要另行通知并等待;本章没有 WebSocket 或长连接的受控实验,标为 NOT_RUN。尚未给 CLI 增加常驻 HTTP 监听入口,真实部署流程也没有启动。客户端对合成下游的超时、响应体关闭及连接池属于 24 的验收;HTTP 服务器层的 Shutdown、后台任务层的 Close 与 SQL 状态提交今后仍需分别记录,不可互相替代。
在 Go 1.27.1/linux-amd64、GOTOOLCHAIN=local、GOMAXPROCS=2、CGO_ENABLED=1、gcc 13.3.0 下,三项新闸门测试重跑 30 次退出 0;两包完整测试、同组与两包 go test -race 均退出 0。无 race 报告只限这轮实际路径,不能据此断言未来所有取消/关闭竞争安全。旧 CLI 的批次结果与零期限错误路径由实际二进制复跑;版本文档、所有真实命令、原始 stdout/stderr 与退出码见 writing-plans/go-language/evidence/23/,不以页面生成代替 Go 或 HTTP 实验。
练习:客户端按 ID 发送取消请求,收到 202 后立刻 GET 得到 RUNNING,是服务“忘了取消”吗?答案:不一定。cancel_requested=true 才表示此刻已记录取消意图,RUNNING 则表示计算尚未交回完成提交点;可继续按 ID 查询终态,不能把 202 当作工作退出确认。若分析器永远忽略取消且不返回,service.Close() 也可能一直等下去,本章闸门正是这种失败路径的受控缩影。排队项不同:它从未执行,可以直接取消为终态。客户端取消自己的 HTTP 请求更不能替代显式任务取消;两条取消信号的所有者根本不同。
任务仍只在内存中;真实 SQL 与重启恢复、其他平台、全站构建含本篇、常驻 HTTP 端口、发布均 NOT_RUN。源码 tag go1.27.1、镜像查询完整 Git SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 沿用基线,官方 Gitiles 交叉核验仍 NOT_RUN。本章只用固定版本公开 API 与受控请求/状态输出解释现象,不依据 runtime 私有调度策略推断任务必然立即退出。
参考资料
http.Request.Context、http.Server.Shutdown:本版go doc原始资料见writing-plans/go-language/evidence/23/,两者的取消/等待范围分别限定。context.CancelFunc:固定工具链文档明确取消请求不等待退出;版本与本章实验边界见writing-plans/go-language/research/23.md。






