这个工程把 HTTP 接收与后台处理拆开:POST 接收任务返回 202,GET 查询状态,工作协程从有界队列处理。它只使用 Go 标准库,展示并发与协议边界;队列和去重记录都在内存中,重启会丢失,不能代替可靠消息系统。
运行与请求
下载 job-lab.zip,需要 Go 1.22 或更高版本。解压后执行:
go test ./...
go vet ./...
在 PowerShell 中设置演示令牌并启动:
$env:JOB_API_TOKEN = 'local-demo-token'
go run .
服务监听 127.0.0.1:8082,只有健康检查允许匿名访问。令牌是本机实验值,服务未配置 TLS;公网使用应在受控 HTTPS 入口后改用实际身份系统。这里授权给单个维护者使用,不提供多用户对象隔离。
curl -i -X POST http://127.0.0.1:8082/jobs -H "Authorization: Bearer local-demo-token" -H "Content-Type: application/json" -H "Idempotency-Key: first-job" --data '{"text":"hello"}'
curl -i http://127.0.0.1:8082/jobs/1 -H "Authorization: Bearer local-demo-token"
处理器把 text 转成大写,返回记录包含 queued、running、done、failed 或 cancelled 状态。真实任务可替换处理函数,但必须遵守 context 取消并给外部副作用设计幂等策略。
接收成功与完成成功不同
202 表示本进程接收了任务,不表示已经完成,更不表示任务已经可靠写入磁盘。若要在接收后断电仍能恢复,应先把任务写入持久存储,再确认接收,并建立认领、租期、重试和去重策略。消息投递解释这些保证。
提交时在一个 Mutex 保护的临界区内检查幂等键、尝试放入有界队列、记录任务与键的关联。相同键与相同 text 返回原 ID;同键换 text 返回 409。队列满返回 503,不留下被拒绝请求的去重记录,所以稍后可再次提交。
队列容量默认 16、工作协程 4 个;最多保存 1024 条任务记录,到上限就拒绝新键。这个上限保护演示程序内存,完成任务不会自动无限保留。生产系统需要明确保留期限、持久存储容量及幂等有效期,不能随意删除键后仍承诺永久去重。
请求取消与任务取消是两个生命周期
提交前使用请求 context,已接收任务改由服务 context 管理,避免客户端关闭页面就取消所有已确认任务。服务关闭时先停止接收,再取消工作 context、把等待任务标记 cancelled,并限时等工作协程退出。这是取消式关闭,不承诺排空全部任务。
如果处理器不响应 context,Close 会在调用者 deadline 到期时返回错误;Go 不能安全地强行杀死任意 goroutine。已经发出的外部写入也不一定随取消回滚,所以 cancelled 不等于外部系统保证没有副作用。
测试怎样制造过载
测试用同步 channel 让第一个任务确定进入处理器并停住,第二个填满容量为 1 的队列,第三个提交必须被拒绝。它不依赖随机 sleep。另一项测试关闭 context,验证工作协程结束、状态改为 cancelled、新提交被拒绝。
HTTP 测试检查缺令牌、严格 JSON、同键重试、同键冲突和状态查询。race detector 需要受支持的平台与相应工具链;可以在具备条件的环境执行 go test -race ./...,普通测试通过不等于已经完成 race 检测。
扩展练习与面试自查
给任务增加最大执行时长,区分请求 deadline、单个任务 deadline 与服务关闭。再用 PostgreSQL 实现持久状态,说明何时返回 202、两个工作者如何避免同时处理同一任务、崩溃后的任务如何重新认领。必须保留原有队列满、重复与取消测试。
- 有界 channel 能否保证任务不会丢?入队和持久提交分别是什么保证?
- 为什么相同幂等键需要比较请求参数?任务结束后什么时候能删除去重记录?
- 若处理器在取消前已经发出扣款请求,任务状态应怎样表达“不确定”?