feat: add durable business inbox example and consumer crash recovery
This commit is contained in:
@@ -114,7 +114,7 @@
|
||||
- 步骤:创建 → 同键核对 → 暂停 → 恢复 → 查询与分页 → 最终暂停。
|
||||
- 预期:duplicate=true、resumed_revision=3、final_enabled=false、revision=4、pending=0。
|
||||
- 保留:每次运行留下一个已暂停订阅及审计;没有硬删除,受64个订阅上限约束。
|
||||
- 范围:本例只演示管理SDK;自动消费 SDK 已实现,持久化Inbox例子继续开发。
|
||||
- 范围:本例只演示管理SDK;自动消费 SDK 已实现,[持久化Inbox例子](result-inbox.md) 已独立交付。
|
||||
|
||||
## 已可运行:结果消费协议与跨连接确认
|
||||
|
||||
@@ -122,3 +122,11 @@
|
||||
- 操作:创建订阅 → 提交并取消任务 → 接收冻结结果 → ACK → 重连重报原证明 → 核对原确认时间 → 暂停订阅。
|
||||
- 输出:ack_replayed=true、same_receipt=true、pending=0、subscription_enabled=false。
|
||||
- 范围:本例只做读取验证,不写业务数据;[自动消费器](../sdk/event-consumer.md) 的生产接入须提供事务Inbox,不能把示例当作业务恰好一次保证。
|
||||
|
||||
## 已可运行:独立业务 MySQL Inbox
|
||||
|
||||
- 独立项目:[result-inbox](../../examples/result-inbox/README.md),[完整前置、命令及故障说明](result-inbox.md)。
|
||||
- 操作:接收冻结结果 → Inbox与业务视图同事务提交 → 重复核对 → ACK → 暂停本次演示订阅。
|
||||
- 输出:inbox_duplicate=true、applied_events=1、ack_confirmed=1、pending=0。
|
||||
- 覆盖:并发重复、旧版本保护、回滚、真实MySQL COMMIT响应丢失、业务提交后/ACK前消费者SIGKILL及新进程去重。
|
||||
- 边界:只覆盖接收方本地SQL效果;核心SDK不依赖数据库,外部副作用需要接收方自己的幂等/Outbox设计。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[完整代码](../../sdk/go/examples/event-protocol/main.go) 使用正式 Go SDK 创建取消结果订阅,提交并取消一个待执行任务,接收并验证冻结结果,然后 ACK;关闭连接后在新连接中重报原证明,验证原确认时间和去重回执,最后暂停本次订阅。
|
||||
|
||||
本例只读取并验证结果,没有业务数据库写入。生产业务应使用 [自动消费器及事务去重约定](../sdk/event-consumer.md)。持久化 Inbox 完整示例尚待交付。
|
||||
本例只读取并验证结果,没有业务数据库写入。生产业务应使用 [自动消费器及事务去重约定](../sdk/event-consumer.md)。持久化接入见 [独立 MySQL Inbox 示例](result-inbox.md)。
|
||||
|
||||
## 启动
|
||||
|
||||
|
||||
109
docs/examples/result-inbox.md
Normal file
109
docs/examples/result-inbox.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 独立业务数据库的结果 Inbox
|
||||
|
||||
[完整示例项目](../../examples/result-inbox/README.md) 演示:结果通过正式消费 SDK 到达接收方后,在自己的 MySQL 中将事件去重记录与任务结果视图一起提交,再允许 SDK ACK。重复事件不会再次推进视图,旧版本不会覆盖新版本。
|
||||
|
||||
示例不连接平台内部存储,不依赖业务 ORM,不创建外键或级联删除。它演示的是本地SQL事务中的去重,不保证任意外部HTTP副作用恰好一次。源码、schema与模型都在独立的 examples/result-inbox 模块内,核心 sdk/go/go.mod 没有新增数据库驱动。
|
||||
|
||||
## 1. 前置与初始化
|
||||
|
||||
需要 Go1.27.1、已启动的 Orbit,以及接收方自己的 MySQL8.4+ 专用空数据库,例如 receiver_example。不要把接收方表建到平台业务库。先用接收方管理员创建数据库并通过安全环境配置注入 ORBIT_RECEIVER_DSN;DSN必须显式包含库名。生产使用受信任TLS及自己的数据库持久化、备份策略。
|
||||
|
||||
初始化所需权限为CREATE TABLE,运行账号只需要这两张接收方表的SELECT/INSERT/UPDATE。应用运行不需要DELETE或修改平台库。示例初始化只执行初始DDL,已有表会报错;不自动修复部分DDL或覆盖已有结构,正式升级应采用接收方自己的前向迁移。
|
||||
|
||||
从仓库根目录准备一个专用示例队列和事件凭据(平台 ORBIT_MYSQL_DSN 已按初始化手册配置):
|
||||
|
||||
```sh
|
||||
mkdir -p .local
|
||||
chmod 700 .local
|
||||
go run ./cmd/orbit-admin bootstrap \
|
||||
--application inbox-example --namespace default --environment development \
|
||||
--queue inbox-demo --event-access --output .local/inbox.credentials.json
|
||||
```
|
||||
|
||||
随后从仓库根目录进入示例项目,ORBIT_RECEIVER_DSN由环境管理器提供:
|
||||
|
||||
```sh
|
||||
cd examples/result-inbox
|
||||
GOWORK=off go run ./cmd/receiver init
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/inbox.credentials.json \
|
||||
GOWORK=off go run ./cmd/receiver demo
|
||||
```
|
||||
|
||||
本地示例用回环HTTP;生产连接使用HTTPS/WSS。GOWORK=off让此独立示例模块不依赖根workspace。首次构建按go.mod/go.sum解析依赖。
|
||||
|
||||
## 2. 演示链路与输出
|
||||
|
||||
demo会创建一个只关心task.cancelled的订阅,提交并取消一个待执行任务,再启动EventConsumer。收到本次任务的事件后,连续两次调用相同业务事务:第一次推进结果视图,第二次核对相同已提交的Inbox记录。处理器返回nil后SDK确认交付,程序检查当前业务视图,再暂停本次订阅。
|
||||
|
||||
```text
|
||||
receiver_schema_initialized=true foreign_keys=0
|
||||
task=<本次任务ID> state=CANCELLED inbox_duplicate=true applied_events=1
|
||||
ack_confirmed=1 pending=0 subscription_enabled=false
|
||||
```
|
||||
|
||||
第一行来自init,后两行来自demo。demo总时限20秒,消费器收尾最多5秒。它只接收本次任务,需用专用隔离队列;队列中另一个程序的取消结果会被拒绝处理,避免演示混用业务数据。
|
||||
|
||||
demo失败时非零退出,保留已受理任务、订阅、交付和业务记录供核查。成功时仅暂停订阅,不硬删历史。每次运行新建身份,命名空间订阅上限64,重复演示宜使用可重建的测试环境。服务端故障和强杀不由demo命令自行注入,专门回归见下文。
|
||||
|
||||
## 3. 持续消费已有订阅
|
||||
|
||||
使用与订阅创建时相同的凭据,提供订阅ID。配置队列必须和该订阅一致;程序启动时先查询并核对,防止配置错队列后不断耗费投递预算。
|
||||
|
||||
```sh
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/inbox.credentials.json \
|
||||
ORBIT_SUBSCRIPTION_ID='<已有订阅的32位ID>' \
|
||||
RECEIVER_NAME=task-result-view \
|
||||
GOWORK=off go run ./cmd/receiver consume
|
||||
```
|
||||
|
||||
ORBIT_RECEIVER_DSN仍需由环境提供。ORBIT_TOKEN、ORBIT_QUEUE_ID可覆盖凭据文件对应值。持续消费需要events:read、events:consume及订阅绑定队列权限;demo另外需要events:manage、tasks:submit和tasks:cancel。程序输出稳定事件分类、事件ID与脱敏存储错误,不输出凭据、结果正文或租约证明。
|
||||
|
||||
RECEIVER_NAME默认task-result-view,最多64个ASCII字符,以字母或数字开头,其余允许字母、数字、点、下划线和连字符。它表示固定的接收用途;重启、扩容、多个实例必须复用同一名称和队列。不同用途是不同业务视图,不能每次启动随机换名来规避重复。
|
||||
|
||||
持续消费并发4,收到SIGINT/SIGTERM时取消在途处理器并最多收尾10秒。只关闭本进程,不暂停共享订阅;其他消费者仍可继续。单次业务事务最多5秒。死锁、数据库不可用、提交不明等返回错误,SDK不ACK,按服务端交付租约及预算恢复。
|
||||
|
||||
## 4. 数据模型与事务
|
||||
|
||||
[schema.sql](../../examples/result-inbox/internal/inbox/schema.sql) 与 [中文模型](../../examples/result-inbox/internal/inbox/model.go) 定义两个实体、17个字段;不计入平台本身的35个实体。
|
||||
|
||||
| 表 | 身份和用途 |
|
||||
| --- | --- |
|
||||
| receiver_inbox | `(consumer_name, queue_id, event_id)`唯一;保存冻结事件、SHA256摘要、是否推进视图和UTC受理时间 |
|
||||
| receiver_task_views | `(consumer_name, queue_id, task_id)`唯一;保存最大任务版本、最后事件/状态/结果、实际推进次数及UTC时间 |
|
||||
|
||||
Apply的事务顺序:
|
||||
|
||||
1. 校验来源队列、事件/任务/版本/结果状态关联;规范化完整事件JSON并计算摘要。对象键和空白顺序不影响摘要;数字使用json.Number保持精度及原十进制表达,时间统一UTC。重复键、非法Unicode、无效JSON、超过512KiB的事件被拒绝。
|
||||
2. 普通INSERT写Inbox唯一键。只对MySQL 1062重复键进入核对;其他错误都返回失败。唯一键竞争等待胜出事务提交或回滚,不把未提交占位当作成功。
|
||||
3. 重复时结束本事务,再查询已提交记录并比较摘要。同身份同内容返回Duplicate=true;不同内容报冲突,不ACK。Result.Applied返回原事务是否推进过视图,不表示本次又执行一次。
|
||||
4. 首次接收时原子建立或锁定单任务视图。事务内占位版本为0,正常提交后不留下占位状态。只接受更高任务版本;旧事件记录为applied=false,保留历史;同任务同版本但不同事件ID视为冲突并整体回滚。
|
||||
5. 更新视图及Inbox的applied标志,在一个READ COMMITTED事务中COMMIT。业务版本、结果及推进次数与Inbox同时提交;成功后Handle才返回nil。
|
||||
|
||||
TaskView是接收方只读结果视图,不是平台权威任务状态。NEEDS_REVIEW必须保留为待核查,不能转译成成功。原事件快照不从后来的GetTask重新拼装。未提供result时保存SQL NULL,JSON大整数不会经float64转换。
|
||||
|
||||
## 5. 故障与核查
|
||||
|
||||
| 窗口 | 行为 |
|
||||
| --- | --- |
|
||||
| 业务更新失败 | 事务回滚,Inbox与视图都不残留;原事件仍可重试 |
|
||||
| COMMIT响应未知 | 返回ErrCommitUnconfirmed,不ACK;之后按原事件身份查Inbox,已提交则去重,未提交则重新处理 |
|
||||
| 业务提交后消费者被强杀 | 原交付未确认;新进程收到补发后使用相同消费者范围命中Inbox,再确认新交付证明 |
|
||||
| 平台ACK回应丢失 | SDK保留原证明重报;进程也退出时,后续补发仍通过Inbox去重 |
|
||||
| 同ID不同内容 / 同任务同版本不同ID | 返回冲突,不自动覆盖或确认;检查来源数据与接收范围,保留原记录 |
|
||||
| 连续失败耗尽预算 | 平台保留EXPIRED并占用窗口;修复根因后通过管理API追加有限预算,不能换消费者名掩盖问题 |
|
||||
|
||||
Inbox保留期必须覆盖平台可补发、人工重试及备份恢复窗口。本例不清理或归档记录;删除去重记录后再次补发可能重新产生业务效果。生产还需要接收方权限、加密、备份恢复及容量治理。
|
||||
|
||||
本地SQL事务无法原子包含外部HTTP、邮件等效果。需要外部副作用时,应在同事务写接收方Outbox,再以稳定业务身份执行和核查;不能在持锁期间调用外部系统。此示例没有冒充通用Outbox框架。
|
||||
|
||||
## 6. 实际回归
|
||||
|
||||
根目录 `make integration` 创建临时MySQL,平台使用orbit_test,接收方使用独立orbit_receiver_test;结束清理容器。示例普通race/vet/build也纳入根Makefile。
|
||||
|
||||
- [存储回归](../../examples/result-inbox/internal/inbox/store_integration_test.go):8个并发相同事件只推进1次、内容冲突、旧版本与范围隔离、业务更新失败整笔回滚、两表17个中文列注释及零外键。
|
||||
- [COMMIT响应丢失](../../examples/result-inbox/internal/inbox/commit_loss_integration_test.go):本地协议代理收到MySQL对COMMIT的真实OK后丢弃并断连,调用方报告提交不明;另一连接验证实际数据已提交,新的Store按相同身份核对,不重复更新。
|
||||
- [消费者真实进程故障](../../examples/result-inbox/internal/inbox/consumer_process_integration_test.go):读取数据库证明业务已提交、平台仍LEASED且未ACK,再SIGKILL消费者并Wait确认退出;新进程在真实30秒租约到期后去重并确认,同一交付attempts=2,业务applied_events仍为1。
|
||||
|
||||
故障屏障仅在测试二进制中提供,正常CLI没有故障开关。COMMIT代理仅处理本地无TLS测试连接,不作为生产组件。测试没有模拟外部业务副作用,也不等同于跨地域或MySQL主从故障切换验收。
|
||||
Reference in New Issue
Block a user