Files
orbit/docs/api/subscriptions.md

333 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 可靠结果订阅 HTTP 与 WebSocket
状态:服务端 HTTP/WSS、管理 SDK、事件连接 SDK、自动重连消费器、持久化 Inbox 示例及真实进程强杀验证均已实现。当前页面说明接口语义;生产积压、跨节点聚合、容量和发布兼容仍按[交付清单](../release-checklist.md)验收。[存储语义与升级边界](../09-result-subscription-core.md),[阶段验证](../reports/phase-3-event-consumer-sdk.md)。
## 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 |
| POST /v1/subscriptions/{id}:retire | events:manage | RetireSubscription |
| 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 | ConnectEvents / NewEventConsumer |
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 去重。
```json
{
"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,不能换键重新追到最新位置。
```json
{
"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,要求以下三个字段全部显式提供:
```json
{
"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:
```json
{
"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:
```json
{
"protocol_version": "1",
"type": "events.hello",
"message_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"body": {"subscription_id": "22222222222222222222222222222222", "capacity": 1}
}
```
capacity 必填整数 1–32,表示该连接允许持有的交付数量,在数据库事务内校验。首版连接期间不可修改,调整容量需要关闭并重新连接。订阅仍可以由同一凭据的多个连接共同消费,各连接竞争持久化交付。
```json
{
"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 是全零字节编码的文档占位值,实际由服务端生成随机秘密:
```json
{
"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 秒租约用于尽快持久化接收结果。
```json
{
"protocol_version": "1",
"type": "event.ack",
"message_id": "dddddddddddddddddddddddddddddddd",
"session_id": "44444444444444444444444444444444",
"body": {
"delivery_id": "33333333333333333333333333333333",
"epoch": "1",
"lease_secret": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
}
```
```json
{
"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 秒。暂停中的订阅仍能连接、心跳和确认已有交付。
```json
{
"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 类型和方法:[类型](../../sdk/go/subscription_types.go)、[管理方法](../../sdk/go/subscriptions.go)。下面是应用函数内片段,client 已由 NewClient 创建,key 应先持久化:
```go
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
```
[完整管理示例](../../examples/subscriptions/main.go) 的启动、输入输出、失败和保留规则见 [场景文档](../examples/subscriptions.md)。它演示管理与幂等,不启动结果消费者。正式 [事件连接 SDK 与自动消费器](../sdk/event-consumer.md) 已提供接收、ACK、心跳、重连与有界收尾;[完整协议示例](../examples/event-protocol.md) 验证跨连接重报原确认。[独立 MySQL Inbox 示例](../examples/result-inbox.md) 已实现并验证消费者进程退出后的去重。
机器可读消息契约见 [events.schema.json](../../protocol/eventsv1/events.schema.json),使用 Draft 2020-12,clientFrame/serverFrame 分方向约束。运行时额外拒绝重复 JSON 键、超范围 uint64 及非规范 base64url。Schema 不代替授权、状态机、租约及事务校验。
## 退役与资源名额
见[通用资源退役](resource-retirement.md)。退役保留订阅和交付历史,不支持重新启用;通过 `retired_at`、`retired_sequence` 区分暂停与退役。未确认、过期或尚未物化的匹配结果都会阻止退役。