18 KiB
可靠结果订阅 HTTP 与 WebSocket
状态:已实现服务端 HTTP/WSS、管理 SDK 和管理示例。事件连接 SDK、自动重连消费器、持久化 Inbox 示例及真实进程强杀验证继续开发,完整 P3 订阅验收尚未完成。存储语义与升级边界,本阶段验证。
1. 授权与共同规则
所有 HTTP 请求使用 Authorization: Bearer 请求头,管理写请求还需要稳定的 Idempotency-Key。生产通过 HTTPS/WSS 接入;本地示例可使用回环 HTTP。凭据、交付秘密不能放在 URL。请求不接受自报 namespace_id、application_id 或 credential_id。
订阅固定绑定创建凭据和一个队列。除动作权限外,每次操作都验证队列范围和订阅所有者。另一张凭据即使属于相同命名空间也不能操作此订阅;凭据重新绑定尚未开放。bootstrap 使用显式 --event-access 才授予三个事件动作。
| 方法与路径 | 权限 | SDK 方法 |
|---|---|---|
| POST /v1/subscriptions | events:manage | CreateSubscription |
| GET /v1/subscriptions?queue_id=... | events:read | ListSubscriptions |
| GET /v1/subscriptions/{id} | events:read | GetSubscription |
| POST /v1/subscriptions/{id}:configure | events:manage | ConfigureSubscription |
| GET /v1/subscriptions/{id}/deliveries | events:read | ListEventDeliveries |
| GET /v1/subscriptions/{id}/deliveries/{delivery_id} | events:read | GetEventDelivery |
| POST /v1/subscriptions/{id}/deliveries/{delivery_id}:retry | events:manage | RetryEventDelivery |
| GET /v1/events/connect(WebSocket Upgrade) | events:consume | 连接 SDK 待交付,可按下文协议接入 |
HTTP 使用统一 request_id + data/error 信封,响应带 X-Request-ID 和 Cache-Control: no-store。管理请求 8 秒服务端超时、JSON 最大 512 KiB。未知字段、大小写别名、重复 JSON 键、未知或重复查询参数均被拒绝。计数/版本以规范十进制字符串传输,不能带正号、前导零或小数。
除两个列表接口外,不接受查询参数。没有公开硬删除、修改过滤、修改历史起点或替换所属凭据的接口。
2. 创建订阅
POST /v1/subscriptions,Content-Type: application/json,Idempotency-Key: 调用方持久化的操作身份。
| 字段 | 要求 |
|---|---|
| name | 必填,1–128 个 ASCII 字符,首字符字母/数字,其余允许字母/数字/点/下划线/连字符;命名空间内唯一 |
| queue_id | 必填,32 位小写十六进制 ID;固定为该队列 |
| types | 可选数组,省略或 [] 表示四类全部;否则必须为四类的非重复子集 |
| from_sequence | 可选 uint64 十进制字符串;省略或 null 从创建时已提交流头之后开始;"0" 从现存结果日志开始 |
| max_pending | 可选整数,省略或 0 使用 100,自定义 1–1000 |
| max_attempts | 可选整数,省略或 0 使用 20,自定义 1–100 |
四类结果为 task.succeeded、task.failed、task.cancelled、task.needs_review。安全重试的 task.requeued 不在此集合。needs_review 不是成功或确定失败;同一个任务以后可能产生另一条权威结果,必须按 event_id 去重,不能只按 task_id 去重。
{
"name": "object-results",
"queue_id": "11111111111111111111111111111111",
"types": ["task.succeeded", "task.failed", "task.cancelled", "task.needs_review"],
"max_pending": 100,
"max_attempts": 20
}
首次成功 HTTP 201;相同身份/内容重报 HTTP 200,deduplicated=true,返回原快照。请求尚未确认时必须保留原键、原请求和原 from_sequence,不能换键重新追到最新位置。
{
"request_id": "77777777777777777777777777777777",
"data": {
"subscription": {
"id": "22222222222222222222222222222222",
"queue_id": "11111111111111111111111111111111",
"credential_id": "88888888888888888888888888888888",
"name": "object-results",
"revision": "1",
"enabled": true,
"types": ["task.cancelled", "task.failed", "task.needs_review", "task.succeeded"],
"cursor_sequence": "12",
"pending_count": 0,
"max_pending": 100,
"max_attempts": 20,
"created_at": "2026-09-17T10:00:00Z",
"updated_at": "2026-09-17T10:00:00Z"
},
"deduplicated": false
}
}
类型集合按固定顺序规范化。cursor_sequence 表示已经扫描并建立交付记录的位置,不是确认水位。pending_count 包含等待、租约中和次数耗尽记录。配置 revision 不随物化/ACK 用量变化。
每命名空间最多 64 个订阅。from_sequence 不能超过已提交流头,也不能早于已清理的保留边界。现存日志从迁移 000013 启用,不补造此前的任务结果。
3. 查询和控制订阅
GET /v1/subscriptions/{id} 返回 data 为上节 subscription 对象,HTTP 200。幂等写回执是原状态;查询接口才读取当前配置和用量。
GET /v1/subscriptions 必须带 queue_id;可选 limit=1–100(省略 20)和 cursor。返回 data.items 为 subscription 数组,另有可选 next_cursor。列表只包含当前凭据在这个队列中的订阅,按 created_at DESC、id DESC 排序。游标不能换队列、凭据或命名空间使用。
POST /v1/subscriptions/{id}:configure 只改变 enabled,要求以下三个字段全部显式提供:
{
"expected_revision": "1",
"enabled": false,
"reason": "receiver maintenance"
}
expected_revision 必须匹配当前配置版本,reason 为非空字符串、最多 1024 个 Unicode 字符。成功 HTTP 200,data 为 SubscriptionResult,即 {subscription, deduplicated}。恢复时设置 enabled=true,并使用新的操作身份与查询取得的版本。重复原命令必须保留第一次的版本,不能把响应的新版本放回原键。
暂停停止新物化/领取,保留已有结果、交付和租约;仍允许消费者确认已可靠保存的结果。暂停不意味着已发消息被撤回。相同布尔值的新命令仍作为新的带审计配置操作增加版本。
4. 交付历史、详情和人工重试
GET /v1/subscriptions/{id}/deliveries 支持 limit、cursor,以及可选 state:PENDING、LEASED、ACKED、EXPIRED。不带 state 返回全部已物化交付。返回 data 为 {items: [...], next_cursor?: "..."},按创建时间及 ID 倒序分页。游标绑定订阅、凭据、命名空间与状态过滤,不能混用。
列表项 EventDelivery 的字段如下:
| 字段 | 类型与含义 |
|---|---|
| id / subscription_id / event_id | 32 位小写十六进制身份;同一事件向同一订阅的交付 ID 不因补发变化 |
| sequence | 来源结果序号,uint64 字符串 |
| revision | 交付并发版本,uint64 字符串;领取、恢复、确认和人工重试时变化 |
| state | 四种交付状态 |
| created_at / available_at | UTC 创建时间和最早可重发时间 |
| lease_epoch | uint64 字符串,从未发送时为 "0",每次领取递增 |
| lease_until | 可选 UTC 租约截止;仅 LEASED 存在 |
| attempts / attempt_budget | 已用次数和累计预算,整数;人工重试不重置 attempts |
| acknowledged_at | 可选 UTC 首次有效确认时间,仅 ACKED 存在 |
GET /v1/subscriptions/{id}/deliveries/{delivery_id} 返回 data 为 {delivery: EventDelivery, event: ResultEvent}。事件字段见下一节的 event.deliver 示例。该接口不返回 lease_secret 或 token_digest,也不允许跨订阅查询同一个交付 ID。
分页是运维查询,不是一致性快照或可靠消费协议。并发状态变化可能改变过滤结果,不能通过列表遍历代替交付确认。未物化结果尚无交付记录,不会出现在该列表。
POST /v1/subscriptions/{id}/deliveries/{delivery_id}:retry 只允许 EXPIRED,带稳定 Idempotency-Key:
{
"expected_revision": "3",
"reason": "receiver persistence failure repaired",
"additional_attempts": 2
}
成功 HTTP 200,data 为 {delivery: EventDelivery, deduplicated: false};交付变为 PENDING,追加 1–100 次预算,累计预算最多 1000000。已有次数、代次、事件身份和积压占用不归零。重报返回原回执,不再追加。
当前租约恢复由启用订阅的后续领取触发;没有消费者或订阅暂停时,租约可能在截止后仍显示 LEASED。查询本身不修改状态,人工重试也不能绕过状态要求。独立后台清扫、历史清理和凭据安全重绑仍待后续完成。
5. WebSocket 建连与边界
入口 /v1/events/connect,Sec-WebSocket-Protocol: orbit.events.v1,Authorization 使用请求头。拒绝 URL 参数及跨来源 Origin;不使用 Cookie。升级前先验证 events:consume,随后 hello 再校验具体订阅所有者和队列范围。读权限不能替代消费权限,消费权限也不隐含读取所有历史的权限。
每帧为一个文本 JSON,最大 512 KiB,禁用压缩。固定 protocol_version="1",message_id 是调用方生成的 32 位随机十六进制标识。服务端响应使用新的 message_id,以 reply_to 关联请求。message_id 只用于相关性;ACK 的持久化去重依据是交付 ID、代次和秘密。
连接后 5 秒内发送 hello,不能带 session_id、reply_to 或 error:
{
"protocol_version": "1",
"type": "events.hello",
"message_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"body": {"subscription_id": "22222222222222222222222222222222", "capacity": 1}
}
capacity 必填整数 1–32,表示该连接允许持有的交付数量,在数据库事务内校验。首版连接期间不可修改,调整容量需要关闭并重新连接。订阅仍可以由同一凭据的多个连接共同消费,各连接竞争持久化交付。
{
"protocol_version": "1",
"type": "events.welcome",
"message_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"reply_to": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"session_id": "44444444444444444444444444444444",
"body": {
"session_id": "44444444444444444444444444444444",
"subscription_id": "22222222222222222222222222222222",
"capacity": 1,
"lease_duration_ms": 30000,
"heartbeat_interval_ms": 15000,
"server_time": "2026-09-17T10:00:00Z",
"enabled": true
}
}
session_id 由服务端生成,仅标识当前连接。后续请求必须带这个值。断线后不恢复旧连接身份,也不删除租约;新的连接使用新 session_id,但可以用上一连接保留的最近证明核对 ACK。
节点最多 128 条事件连接。领取池全局最多 4 个在途、每应用最多 1 个;等待加在途最多 128,按认证应用及订阅轮转。每连接读队列 4、写队列 8,每个扫描轮次最多排入 4 次领取。慢 SQL 领取和控制收发分开;领取权及容量仍由 MySQL 决定。
这些是当前节点保护参数,不是生产吞吐承诺,也没有跨节点全局公平或应用连接席位预留。Worker 连接使用独立入口、协议与连接池。
6. 交付与可靠确认
服务端先持久化交付租约,再发送 event.deliver。以下 lease_secret 是全零字节编码的文档占位值,实际由服务端生成随机秘密:
{
"protocol_version": "1",
"type": "event.deliver",
"message_id": "cccccccccccccccccccccccccccccccc",
"session_id": "44444444444444444444444444444444",
"body": {
"delivery": {
"created_at": "2026-09-17T10:00:00Z",
"id": "33333333333333333333333333333333",
"subscription_id": "22222222222222222222222222222222",
"event_id": "55555555555555555555555555555555",
"sequence": "13",
"revision": "2",
"state": "LEASED",
"available_at": "2026-09-17T10:00:00Z",
"lease_epoch": "1",
"lease_until": "2026-09-17T10:00:30Z",
"attempts": 1,
"attempt_budget": 20
},
"event": {
"id": "55555555555555555555555555555555",
"sequence": "13",
"type": "task.cancelled",
"aggregate_kind": "task",
"aggregate_id": "66666666666666666666666666666666",
"aggregate_revision": "2",
"occurred_at": "2026-09-17T10:00:00Z",
"details": {"state": "CANCELLED", "reason": "CANCEL_REQUESTED"},
"snapshot": {
"schema_version": 1,
"task_id": "66666666666666666666666666666666",
"queue_id": "11111111111111111111111111111111",
"state": "CANCELLED",
"revision": "2",
"effect_uncertain": false
}
},
"lease_secret": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
}
snapshot 还可包含业务 result 和 current_attempt_id;result 用原始 JSON 文本保留数值精度。details 是来源事件的元信息,按事件类型演进;不含任务输入或交付秘密。结果快照不可变。
接收方应在本地事务中使用 event.id 作为 Inbox 唯一键,完成可靠保存或业务事务后才确认。不得先 ACK 再把消息仅放到内存队列。耗时业务可先可靠写入本地 Outbox,再由业务侧继续处理。平台首版不提供交付续租;30 秒租约用于尽快持久化接收结果。
{
"protocol_version": "1",
"type": "event.ack",
"message_id": "dddddddddddddddddddddddddddddddd",
"session_id": "44444444444444444444444444444444",
"body": {
"delivery_id": "33333333333333333333333333333333",
"epoch": "1",
"lease_secret": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
}
{
"protocol_version": "1",
"type": "ack",
"message_id": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"reply_to": "dddddddddddddddddddddddddddddddd",
"session_id": "44444444444444444444444444444444",
"body": {
"delivery_id": "33333333333333333333333333333333",
"event_id": "55555555555555555555555555555555",
"epoch": "1",
"acknowledged_at": "2026-09-17T10:00:01Z",
"deduplicated": false
}
}
确认响应丢失时,保留原 delivery_id、epoch 和 lease_secret 重报;可以在新连接中使用新的 session_id 重报。已确认时返回原 acknowledged_at 和 deduplicated=true,不再次扣减积压或写重复确认审计。
租约到期后,最新证明若尚未被新代次替换,仍可迟到确认。新的发送产生后旧证明返回 DELIVERY_LEASE_LOST;接收方已保存的 Inbox 仍保留,下一次收到同一事件时检查已有记录,再用新交付证明 ACK。不能把旧证明替换为猜测的新代次。
交付是至少一次语义;连接关闭、写超时、ACK 响应丢失都可能导致重复。不得依靠本地内存“见过”集合宣称业务恰好一次。
7. 心跳、错误和关闭
客户端建议每 15 秒发送一次 events.ping,body 为 {};服务端以 ack 回复 {server_time, enabled},同时重新校验凭据与订阅范围。收到结果也不能替代客户端心跳。连续 60 秒没有有效读取到客户端帧会关闭连接;写入超时为 5 秒。暂停中的订阅仍能连接、心跳和确认已有交付。
{
"protocol_version": "1",
"type": "events.ping",
"message_id": "ffffffffffffffffffffffffffffffff",
"session_id": "44444444444444444444444444444444",
"body": {}
}
错误帧使用 type="error" 与 error={code,message},无 body。请求错误带 reply_to;后台领取发现授权失效或其他错误时可以没有 reply_to。hello 绑定失败时没有 session_id,绑定后的错误带当前 session_id。非法信封、二进制消息或错误会话直接按 WebSocket 1008 关闭,超限消息由 WebSocket 关闭。
| 错误码 | HTTP 状态 / WSS 行为 |
|---|---|
| INVALID_ARGUMENT | 400;命令体无效时可返回 WSS error,非法信封直接关闭 |
| UNAUTHENTICATED | 401;已连接时发送错误并关闭 |
| PERMISSION_DENIED | 403;已连接时发送错误并关闭 |
| NOT_FOUND | 404;订阅/交付不在凭据与父资源范围;连接内发生时关闭 |
| STATE_CONFLICT | 409;版本、状态或累计预算不允许操作 |
| IDEMPOTENCY_CONFLICT | 409;同键内容不同 |
| SUBSCRIPTION_LIMIT | 409;命名空间订阅数达到 64 |
| EVENT_HISTORY_EXPIRED | 410;WSS 领取发现历史缺口则报错并关闭,不静默跳到最新位置 |
| DELIVERY_LEASE_LOST | WSS ACK 证明已替换或无效;连接保留,等待新交付或核对历史 |
| PROTOCOL_REQUIRED | 升级前 426,必须指定 orbit.events.v1 |
| CAPACITY_EXCEEDED | 升级前 429,Retry-After: 1 |
| UNAVAILABLE | 503;WSS 中数据库等临时失败会报错并关闭,重连仍保留原交付/确认身份 |
断线/节点关闭不清除持久化交付。领取恢复会在预算内补发,或保留 EXPIRED。客户端关闭时应先完成自己的本地持久化、保存未核对 ACK 的证明,再关闭连接;平台不能保证物理撤回已经写入网络的消息。
8. 管理 SDK 与完整可运行示例
已实现的 Go 类型和方法:类型、管理方法。下面是应用函数内片段,client 已由 NewClient 创建,key 应先持久化:
created, err := client.CreateSubscription(ctx, key, orbit.CreateSubscriptionRequest{
Name: "object-results",
QueueID: queueID,
MaxPending: 100,
MaxAttempts: 20,
})
if err != nil { return err }
current, err := client.GetSubscription(ctx, created.Subscription.ID)
if err != nil { return err }
_, err = client.ConfigureSubscription(ctx, current.ID, pauseKey, orbit.ConfigureSubscriptionRequest{
ExpectedRevision: current.Revision,
Enabled: false,
Reason: "receiver maintenance",
})
return err
完整管理示例 的启动、输入输出、失败和保留规则见 场景文档。它演示管理与幂等,不启动结果消费者。正式 事件连接 SDK 与自动消费器 已提供接收、ACK、心跳、重连与有界收尾;完整协议示例 验证跨连接重报原确认。独立 MySQL Inbox 示例 已实现并验证消费者进程退出后的去重。
机器可读消息契约见 events.schema.json,使用 Draft 2020-12,clientFrame/serverFrame 分方向约束。运行时额外拒绝重复 JSON 键、超范围 uint64 及非规范 base64url。Schema 不代替授权、状态机、租约及事务校验。