Files
orbit/docs/09-result-subscription-core.md

95 lines
9.9 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.
# 可靠结果订阅:持久化核心
状态:P3 的存储与服务层已实现并通过本地全量回归,见 [验证记录](reports/phase-3-result-subscription-core.md)。[HTTP/WSS、管理 SDK 与管理示例](api/subscriptions.md) 已接入。[事件连接 SDK 与自动消费器](sdk/event-consumer.md) 已实现,[持久化 Inbox 示例](examples/result-inbox.md) 及消费者强杀回归已完成;本页的存储方法属于服务端内部模块。
## 1. 已实现范围
结果来源限定为 task.succeeded、task.failed、task.cancelled、task.needs_review。安全重试产生的 task.requeued 不表示最终结果,不进入这个结果流。needs_review 表示需要核查,可能以后再产生权威结果,接入方不能把它当作成功或最终失败。
来源任务状态、原状态事件、不可变结果快照、结果流序号、原执行回执/审计在同一数据库事务中提交。任何一项失败都会回滚,Worker 不会收到“结果确认成功但没有可靠通知记录”的回执。
`orbit_events` 继续保存状态事件;新增 `orbit_result_events` 使用相同事件 ID 保存队列、提交序号和快照。快照包含 schema_version=1、task_id、queue_id、state、revision、result、effect_uncertain 和可空 current_attempt_id。不包含任务输入和租约秘密。结果使用 JSON 文本保存,保留大整数的原始数值文本。来源任务之后发生改变时,不会重新生成已发布的快照。
## 2. 为什么不能按最大事件 ID 推进
自增 ID、随机 ID 和事件创建时间都不能证明事务提交顺序。如果较小 ID 的事务还没有提交,消费者先看到较大的 ID 并移动游标,就可能永久漏掉较小的事件。
`orbit_event_streams` 为每个命名空间保存 last_sequence。生产事务持有独立流行锁,分配下一个序号并一起提交。后来事务在前一个提交或回滚前,不能获得更大的可见序号。回滚不消耗序号。
消费者读取已经提交的流头,并且只扫描不超过这个位置的日志。读取流头不加锁;订阅创建需要锁流行,以确定“从现在开始”的准确边界。该方案存在命名空间内的短暂串行分配热点,其实际吞吐和锁等待仍须在 P6 验证。
锁顺序:
- 任务路径沿用认证/应用额度/队列/任务/尝试/许可的既定顺序,保存结果时最后取得流行锁;不访问订阅行。
- 创建订阅按认证 → 幂等身份 → 队列共享锁 → 流行锁 → 新订阅/审计执行。
- 消费、确认和管理按认证 → 必要的幂等身份 → 订阅 → 交付执行;不再取得任务、队列、应用额度或流行锁。
## 3. 订阅和授权
每个订阅绑定创建凭据和一个队列,事件类型集合在创建时冻结。名字限 1–128 个 ASCII 字符,以字母或数字开头,其余允许字母、数字、点、下划线和连字符;命名空间内唯一。首版每命名空间最多 64 个订阅,没有公开删除或更换所属凭据的能力。
| 字段 | 语义 |
| --- | --- |
| from_sequence | 订阅此序号之后的结果;省略时使用创建事务确定的最新提交序号;显式 0 表示从现存结果日志开始 |
| types | 四类结果的非空子集;省略或空数组使用全部四类,重复或其他类型被拒绝 |
| max_pending | 已物化但尚未确认的交付上限,省略或 0 使用 100,自定义范围 1–1000 |
| max_attempts | 每条新交付初始可发送次数,省略或 0 使用 20,自定义范围 1–100 |
| cursor_sequence | 已扫描并可靠建立交付记录的位置,**不是消费者已确认位置** |
| pending_count | PENDING、LEASED、EXPIRED 的总数;每次有效确认只扣减一次 |
| revision | 配置并发版本;物化和积压数量变化不增加该版本 |
新动作独立授权:events:manage、events:read、events:consume。每个操作同时检查凭据有效性、所属应用/命名空间启用、队列范围和订阅所有者。同一命名空间的另一张凭据,也不能读取、领取、确认或管理该订阅。管理权限不能隐含消费权限,任务读取权限不能隐含订阅权限。
bootstrap 新增显式 --event-access,用于签发包含这三项动作的开发凭据;不会给既有凭据自动扩权。轮换后的安全重新绑定列入 P6,当前不能将旧凭据所属订阅自动转移。
创建、暂停/继续、人工重试都有持久化幂等身份及原回执。身份作用域包含所属凭据与目标资源,复合身份使用 SHA-256 编码到 64 字符,避免跨凭据回执泄漏或数据库截断。
## 4. 从结果日志到交付记录
每次领取在订阅锁下最多扫描 100 条本队列已提交结果,再按冻结类型过滤。选中的事件先创建唯一的 `(namespace_id, subscription_id, event_id)` 交付,随后推进 cursor_sequence,两者同事务提交。
达到 max_pending 后停止物化。未物化的结果保留在来源日志;任务生产者不锁订阅、也不等待消费者腾出交付窗口。有效确认释放空间后,后续扫描继续追赶。每条交付独立确认,处理顺序可以不同,不靠单个最大 ACK 序号跳过中间事件。
这是有界交付窗口,不是“所有存储都已限制大小”。首版结果日志和已确认交付不自动删除;持久化积压仍占数据库空间。清理、水位推进、容量告警及运维治理需要后续 P6 完成。
## 5. 租约、确认与恢复
| 状态 | 含义与转换 |
| --- | --- |
| PENDING | 等待就绪;领取时持久化租约后转 LEASED |
| LEASED | 已生成一次发送授权;有效确认转 ACKED,超时后按预算转 PENDING 或 EXPIRED |
| ACKED | 已可靠确认;重复确认返回原确认时间,不再次扣减 pending_count,也不重复写确认审计 |
| EXPIRED | 已耗尽交付预算,仍保留事件和积压占用;允许带原因的人工追加预算,或接受尚未被替换的最近证明的迟到确认 |
租约固定 30 秒,每次领取增加 lease_epoch 和 attempts,并产生新的 32 字节随机秘密。数据库只保存摘要;确认时使用常量时间摘要比较。当前内部领取按服务端连接 ID 计算容量,单连接容量 1–32。连接 ID 由传输层生成,不能接受客户端自报来绕过容量。
下一次启用状态下的领取,会先回收该订阅的过期交付。未耗尽时使用 `min(2^(attempts-1), 30)` 秒退避;耗尽时转 EXPIRED,不无限自动发送。当前没有独立订阅清扫进程:无人消费或订阅暂停期间,租约状态可能保持 LEASED,直至下一次领取执行恢复。
ACK 表示消费者已可靠保存或处理结果,不授予新业务执行权。当前最后一次秘密和代次即使过期,只要没有被新的交付替换,仍可用于迟到确认;新交付产生后,旧证明统一返回 DELIVERY_LEASE_LOST。确认和重新交付竞争通过订阅锁串行化,只会有一方有效。
服务端不能保证消费者业务恰好执行一次。[独立接收方示例](examples/result-inbox.md) 展示以 event_id 建立唯一 Inbox,在本地事务中保存结果/业务变更,提交后才 ACK。断线和 ACK 响应丢失可能造成重复投递,调用方不得按“收到过一次”就直接丢弃尚未可靠保存的内容。
每次交付授权、首次有效确认、订阅管理和人工重试均记录审计。交付审计包含事件标识、代次、次数、连接和租约截止,不记录交付秘密或结果正文。
## 6. 管理与人工重试
暂停只停止新的物化和领取,不撤回已经发出的结果。暂停期间仍接受有效 ACK。恢复后继续处理原有积压,事件和交付 ID 不变。
人工重试仅适用于 EXPIRED,必须提供 expected_revision、非空 reason 和 additional_attempts(1–100)。追加后的累计预算最多 1000000;已使用次数、当前代次和原事件身份均不归零。新领取才产生新代次。相同幂等键重报返回原响应,不重复加预算。
当前服务层方法见 [platform/events/service.go](../platform/events/service.go),持久化实现见 [订阅](../internal/storage/mysql/event_subscriptions.go)、[交付](../internal/storage/mysql/event_deliveries.go) 和 [结果日志](../internal/storage/mysql/result_journal.go)。这些是服务端内部模块,独立 SDK 不应直接依赖它们。
## 7. 迁移与历史边界
[000013](../internal/storage/mysql/migrations/000013_result_subscriptions.up.sql) 新增两个实体,并扩展订阅和交付字段。当前共 35 个实体、289 个直接声明的持久化字段,全部中文注释;新增或调整的 SQL 列提供中文 COMMENT。没有新增外键或级联删除。
升级必须停止旧版本写入节点,应用迁移,再启动支持新结果日志的版本。旧的运行中写入代码不会补写结果日志,不能在启用订阅后继续混跑。迁移沿用显式版本、校验和、dirty 标记和就绪检查,不自动修改历史迁移。
迁移前的 orbit_events 原样保留,不补造无法证明正确的旧结果快照。from_sequence=0 只覆盖迁移启用后的结果日志,不代表系统建立以来全部任务。历史占位订阅缺少可靠队列绑定,升级后保留但禁用,不对外消费;历史交付保留原次数和代次,预算至少等于已用次数。
retained_from_sequence 初始且目前固定为 1。未来实现清理后,如果订阅游标落在保留边界以前,必须返回 EVENT_HISTORY_EXPIRED,不得静默跳到最新结果。请求未来序号也会被拒绝。
## 8. 下一步
HTTP 创建/查询/分页、暂停/继续、交付与耗尽记录查询、人工重试,以及 orbit.events.v1 WSS、消息 Schema、管理 SDK 和管理示例已实现,见 [接口手册](api/subscriptions.md)。[事件连接 SDK 和自动消费器](sdk/event-consumer.md) 已实现;[持久化 Inbox 去重示例](examples/result-inbox.md) 与消费者进程故障回归已完成。可靠订阅各层均有对应证据,P3其他功能缺口见开发计划。