# Go Worker 底层协议 SDK 更新:2026-09-17。已实现可独立构建的 `WorkerConnection`,配合真实 Orbit 服务和 MySQL 通过控制操作、WSS、ACK 丢失与租约过期回归。本页是底层接口,不自动启动处理器、心跳、续租、重连或排空;普通持续消费使用已实现的 [自动 Worker 运行器](worker-runtime.md)。 源码:[连接与控制方法](../../sdk/go/worker_connection.go)、[中文注释的数据契约](../../sdk/go/worker_types.go)。传输字段、权限和事务语义见 [Worker 接口](../api/worker.md)。SDK 独立 module,只依赖标准库和锁定版本的 `github.com/coder/websocket v1.8.15`,不导入服务端包。 ## 1. 可运行的文本规范化示例 需要已完成迁移、已启动的 Orbit 和具有 `workers:connect` / `workers:consume` 权限的专用凭据。在仓库根目录运行: ```sh mkdir -p .local chmod 700 .local go run ./cmd/orbit-admin bootstrap \ --application worker-example --namespace default --environment development \ --queue normalization --worker-access --output .local/worker.credentials.json cd sdk/go ORBIT_URL=http://127.0.0.1:8080 \ ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \ go run ./examples/worker-protocol ``` 预期输出: ```text task=<32位任务ID> final_state=SUCCEEDED attempts=1 result={"text":"HELLO ORBIT"} ``` [完整源码](../../sdk/go/examples/worker-protocol/main.go) 实际提交 `{"text":" Hello Orbit "}`,领取并取得开始授权,执行去首尾空格/转大写,报告结果、查询当前状态并排空。没有生成文件或调用外部服务。每次运行创建独立任务与临时处理器名,避免消费前次示例遗留任务;历史和审计保留,不硬删除。 安全重试可通过 `WorkerFinish.Retryable=true` 声明明确失败允许再试,仅适用于 WorkerFailed;省略为 false。新尝试由服务端决定,详见 [安全重试](../api/retries.md)。 示例最多对开始/结果操作重连重报两次,保持原进程身份、尝试证明、消息 ID 和内容;业务函数只在首次取得有效开始确认后调用一次。网络结果仍不明时非零退出,不能推断服务端回滚。身份与待确认结果只保留在当前进程内,进程被终止后的未知结果由服务端回收进入核查。本示例处理很短的纯计算,不演示长任务自动续租。 ## 2. 创建进程身份与连接 以下为接入函数内片段;完整错误处理见示例。 ```go registration, err := orbit.NewWorkerRegistration(queueID, 4, []orbit.WorkerCapability{ {Handler: "text.normalize", HandlerVersion: "v1", PayloadVersion: "v1"}, }) if err != nil { return err } conn, err := client.DialWorker(ctx, registration) if err != nil { return err } defer conn.Close() ``` 同一进程只生成一次 registration;重连复用,进程重启换新值。它固定凭据、队列、能力集合和实例秘密。不要把身份和秘密复制给另一个活跃进程:新会话会隔离旧会话。 `DialWorker` 的 ctx 只控制握手,最长 8 秒,调用方配置的 HTTP 超时可以更短;成功返回后需主动 Close。URL 基于 `NewClient` 的 HTTP(S) 地址,分别建立 WS/WSS,禁止重定向,不关闭 TLS 证书校验,不把 token 放入 URL。只支持 `pulse.worker.v1` 子协议,禁用压缩。 `Welcome()` 返回首次连接快照;`Done()` 关闭表示读取循环已退出;`Err()` 返回关闭原因。`Close()` 立即停止传输,不撤销外部已发出的请求,也不代替排空确认。 注册和尝试证明包含秘密,JSON 序列化只用于协议。SDK 的 `String` / `GoString` 隐藏注册、尝试、派发和结果中的秘密/业务数据;直接读字段、JSON 日志和调试器仍可能泄露,接入方应只记录任务 ID、尝试 ID 和错误分类。 ## 3. 方法清单 除 Welcome/Done/Err/Close,所有连接操作第一个参数都是 context。控制操作同时要求调用方提供 32 位小写十六进制 message ID。 | 方法 | 输入 | 返回 | 注意事项 | | --- | --- | --- | --- | | `NewWorkerRegistration` | queueID、capacity、capabilities | WorkerRegistration | 生成随机进程身份与秘密,复制/排序能力,拒绝重复能力 | | `NewWorkerMessageID` | 无 | string | 只为新的逻辑操作生成;未知结果重报复用旧值 | | `client.DialWorker` | registration | *WorkerConnection | 握手后尚未启用派发 | | `conn.Capacity` | messageID、WorkerCapacity | WorkerWelcome | 总容量覆盖值;同时续期会话 | | `conn.NextAssignment` | ctx | WorkerAssignment | 只表示预占,不能直接运行处理器 | | `conn.NextCancellation` | ctx | WorkerCancellation | 注册 CancellationNotifications=true 后持续读取,只停止匹配 task/attempt/epoch 的处理器 | | `conn.Start` | messageID、WorkerAttempt | WorkerCommandResult | 持久化开始后确认,检查 Lease 的当前授权 | | `conn.Renew` | messageID、WorkerAttempt | WorkerCommandResult | 新续租用新消息;原消息只核对原操作 | | `conn.Finish` | messageID、WorkerFinish | WorkerCommandResult | 区分权威结果 APPLIED 与证据 EVIDENCE_ONLY | | `conn.Resume` | messageID、WorkerAttempt | WorkerLease | 重连核对原尝试,不创建尝试、不延长租约 | 同连接内 `Capacity.Sequence` 从 1 开始递增;同序号必须同容量/排空标志,不能用增量值。容量为零停止新领取,不释放在途许可。首次确认容量才开启派发,所以重连时可以先 Resume 所有已知尝试,再发布容量。会话与任务续期是两件事:Capacity 不延长任务租约,Renew 不代替会话心跳。 排空使用 `Drain:true`。排空在同进程重连后保留,后续心跳也必须带 true;尝试清除会返回 `STATE_CONFLICT`。排空不禁止收集已发请求结果。 ## 4. 当前授权与本地期限 ```go assignment, err := conn.NextAssignment(ctx) if err != nil { return err } ref := assignment.Attempt() startID, err := orbit.NewWorkerMessageID() if err != nil { return err } started, err := conn.Start(ctx, startID, ref) if err != nil { return err } deadline, active := started.Lease.ExecutionDeadline() if !active { return errors.New("attempt has no current execution authority") } work, cancel := context.WithDeadline(ctx, deadline) defer cancel() // 将 work 与业务输入传给处理器,不传执行秘密。 ``` `ExecutionDeadline()` 只认可 `Active=true`、`State=RUNNING`、CancelRequestedAt 为空和仍未到期的本地期限。取消后 Active 仍可为 true,用于报告当前结果,不授予继续执行权。SDK 以服务端 `lease_until-server_time` 为窗口,扣除从调用开始起的全部耗时(含等待写入)及 250 毫秒安全余量,用本地单调时钟表示。数据库墙钟与客户端相差一天也不会额外获得一天授权。 只有 SDK 控制响应会设置内部本地期限;自行构造/JSON 解码的 WorkerLease 不会得到执行授权。旧 start 回执即使包含历史 RUNNING 和未来 lease_until,也不能越过当前 Lease 的失效判断。 此底层接口不会替调用方更新处理器上下文。长任务必须有独立的续租与到期取消机制,断线期间也要在最后确认的本地期限停止推进。新的 Resume/Start 回执不能复活一个已因租约到期而停止的处理器。Go 无法强杀不理会 context 的 goroutine,SDK 更不能撤回外部请求;业务幂等/执行代次隔离仍需在接入方落实。 ## 5. 重报与结果分类 | 情况 | 正确处理 | | --- | --- | | Start 请求结果不明,业务尚未开始 | 保留 startID;重连后 Resume,再用原 startID 核对;核验当前授权后最多开始一次 | | 处理器已在当前进程运行,随后断线 | 不再次调用处理器;保留它的已确认租约期限,重连核对原尝试 | | Finish 请求结果不明 | 保留原 finishID、Outcome、Result、ErrorCode,重连核对后原样重报 | | 明确业务成功 | `WorkerSucceeded`,不带 ErrorCode | | 可确认的业务失败 | `WorkerFailed`,带稳定 ErrorCode | | 收到取消意图,确认已停止且本次无未结清效果 | `WorkerCancelled`,带稳定 ErrorCode;普通 context 错误不足以证明 | | 超时、失联或不能确认外部效果 | `WorkerUnknown`,带稳定 ErrorCode;默认转核查,仅 IDEMPOTENT 且预算允许时自动重试 | | `Disposition=EVIDENCE_ONLY` | 内容已留作证据;不能当作任务成功,另查 GetTask 的权威状态 | Result 是不超过 256 KiB 的非 null JSON,可传对象引用;原始异常不应写入稳定错误分类。SDK 拒绝非法 Unicode、重复键和过深 JSON,保留数字文本;数值范围仍由服务端最终校验。 ## 6. 错误与有界行为 - 握手 HTTP 拒绝返回 `*HTTPError`;业务控制拒绝返回 `*WorkerError`,通过 Code 分支处理。Error() 只显示分类,避免未知错误正文进入日志。 - Start 的 RATE_LIMITED 表示当前窗口不允许开始;保留原消息身份在剩余租约内退避,不能调用业务处理器。队列控制、降额与窗口定义见 [队列接口](../api/queues.md)。 - 传输中断返回 `ErrWorkerDisconnected`;消息格式/版本不兼容返回 `ErrWorkerProtocol`;主动关闭为 `ErrWorkerClosed`;超时/取消支持标准 context 错误。 - 最多 32 个在途控制请求,达到上限返回 `ErrWorkerBusy`,请求未发送。同一消息 ID 已在途时也拒绝第二个并发等待者。 - 派发缓冲最多 8 条;读取不及时会以 `ErrWorkerBackpressure` 关闭连接。持久化预占由服务端恢复,不无限积压内存。 - 取消通知另有 32 条缓冲,满时同样关闭连接;NextCancellation 的等待 context 取消不会断开连接。重连后仍要通过 Resume/Renew 核对取消意图,通知不代替持久化状态。完整契约与先升级服务端的要求见 [运行中取消](../api/cancellation.md)。 - 控制操作最长 10 秒,包含排队、发送与等待 ACK;调用方 context 可以缩短。已发送后取消/超时会关闭连接,操作是否提交需用原身份核对。 - `NextAssignment` 的 context 取消只停止等待,不关闭连接。已关闭连接不会继续交付缓冲中的派发。 - 入站单帧最多 512 KiB;读空闲期限 60 秒。需要调用方持续心跳,不能用此期限代替默认 45 秒会话期限。 以上是实现上限与正确性保护,不是容量承诺。测试命令与真实覆盖见 [P2 SDK 协议回归](../reports/phase-2-sdk-protocol.md)。