Files
orbit/docs/api/subscriptions.md

18 KiB
Raw Blame History

可靠结果订阅 HTTP 与 WebSocket

状态:服务端 HTTP/WSS、管理 SDK、事件连接 SDK、自动重连消费器、持久化 Inbox 示例及真实进程强杀验证均已实现。当前页面说明接口语义;生产积压、跨节点聚合、容量和发布兼容仍按交付清单验收。存储语义与升级边界,阶段验证。

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 不代替授权、状态机、租约及事务校验。