feat: add durable business inbox example and consumer crash recovery

This commit is contained in:
2026-09-17 20:49:51 +08:00
parent b17d9b1977
commit a8a8dcfe1e
27 changed files with 1601 additions and 17 deletions

View File

@@ -0,0 +1,83 @@
# P3 业务 Inbox 与消费者故障回归
验证日期:2026-09-17。交付 [独立 MySQL Inbox 示例](../examples/result-inbox.md)。可靠订阅已具备生产者事务日志、HTTP/WSS、管理及消费 SDK、事务接收示例与对应故障证据。P3仍需补齐原功能范围中的批量受理、独立任务核查/人工重试、查询筛选和业务Outbox接入,持续目标保持进行中。
## 1. 交付与模块边界
- [独立示例模块](../../examples/result-inbox/go.mod) 引用正式SDK和MySQL驱动,不依赖平台内部模型/存储,不给核心SDK增加数据库依赖。GOWORK=off可单独构建、测试和运行。
- [业务存储](../../examples/result-inbox/internal/inbox/store.go) 以固定消费者用途、队列、事件身份去重;冻结文档摘要冲突拒绝确认。Inbox与任务结果视图同事务提交,视图仅接受更高任务版本。
- [接收方模型](../../examples/result-inbox/internal/inbox/model.go) 与 [独立SQL](../../examples/result-inbox/internal/inbox/schema.sql) 包含2个实体、17个字段,Go与SQL均有中文注释,无外键和级联删除。平台本身仍是35个实体、289个直接声明持久化字段,未增加平台迁移。
- [可运行CLI](../../examples/result-inbox/cmd/receiver/main.go) 提供init、demo、consume;初始化不覆盖已有表,持续消费核对订阅队列,演示核对业务重复、ACK及订阅暂停。
- Makefile加入独立示例的fmt/race/vet/build,统一集成脚本单独创建orbit_receiver_test数据库,与平台orbit_test隔离。
## 2. 验证环境与结果
macOS arm64、Go1.27.1、Docker MySQL8.4.10、一次性数据库、回环HTTP/WSS、真实Orbit和消费者子进程。数据库代理只用于本地无TLS的COMMIT故障注入,测试不会连接既有业务库;脚本结束清理自己的容器和临时凭据。
```sh
make fmt test vet build
make integration
cd examples/result-inbox
GOWORK=off go test -race ./...
GOWORK=off go vet ./...
```
| 分组 | 本次结果 |
| --- | --- |
| 全量平台MySQL集成 | PASS,91.331秒 |
| SDK Worker连接/自动运行器 | PASS,87.413秒 |
| SDK计划 / 队列 / 订阅管理 / 应用额度 | PASS,5.637 / 4.449 / 33.392 / 1.395秒 |
| SDK事件连接与自动消费 | PASS,50.959秒 |
| 新业务接收方六项集成 | PASS,35.512秒 |
| 其中真实COMMIT响应丢失 | PASS,0.02秒 |
| 其中消费者提交后/ACK前SIGKILL | PASS,34.20秒;使用真实30秒交付租约 |
| 原九项真实进程故障 | PASS,60.953秒;加上新消费者故障共十项 |
| 完整接入示例 | 15个全部通过,包括新的独立Inbox demo |
| 根及独立示例fmt/race/vet/build | PASS |
第一次专项启动脚本使用了不存在的测试名,导致未建平台表而失败;修正为实际的TestIntegrationSchemaAndConstraints后,专项及上表全量回归通过。没有把“无测试运行”的输出当成验证证据。
实际demo输出:
```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
```
## 3. 正确性与数据证据
[真实存储测试](../../examples/result-inbox/internal/inbox/store_integration_test.go) 以8个并发事务处理同一事件:1次首次处理、7次已提交重复,Inbox只有1行,业务applied_events=1。相同事件ID改动内容返回冲突,原结果及推进次数保持不变。
迟到的低版本事件保存Inbox历史但不回退视图;同任务同版本不同事件ID整体回滚。不同消费者用途有独立去重范围,错队列被拒绝。测试用MySQL触发器使业务更新实际失败,验证Inbox及占位视图一起回滚,移除注入后原身份可重新提交。
[文档身份测试](../../examples/result-inbox/internal/inbox/store_test.go) 验证对象键顺序、空白和同一时刻的时区表达不改变摘要;业务数字变化会改变摘要。保留9007199254740993和HTML字符,拒绝重复JSON键、无效JSON/UTF-8和未配对UTF-16转义。
真实information_schema断言接收方2张表、17个中文列注释、0个外键。平台原v7升级回归继续确认70个历史外键最终为0、325项原索引列定义保留。
## 4. MySQL COMMIT响应丢失
[代理测试](../../examples/result-inbox/internal/inbox/commit_loss_integration_test.go) 转发真实MySQL握手、事务和语句。收到COMMIT对应的成功OK包后,代理丢弃该包并断开连接;没有伪造成功响应,也没有直接修改表制造已提交状态。
Apply返回ErrCommitUnconfirmed而非成功。测试用另一条直连查询确认Inbox及业务视图确实已经提交,再用新建Store处理同一冻结事件:命中相同摘要,返回Duplicate=true,业务推进次数保持1。测试日志中的MySQL unexpected EOF是这次有意断连的结果。
这证明本地事务提交不明时按稳定事件身份核对的路径,不是MySQL主从切换、磁盘故障或跨地域复制验收。
## 5. 消费者真实进程强杀
[消费者子进程测试](../../examples/result-inbox/internal/inbox/consumer_process_integration_test.go) 使用正式EventConsumer与Store:
1. 首个进程在Apply成功后停在测试屏障,尚未从处理器返回nil,因此SDK未发送ACK。
2. 父进程查询业务数据库,确认Inbox与视图均已提交、applied_events=1;查询平台,确认交付仍LEASED且acknowledged_at为空。
3. SIGKILL消费者,用Wait确认其确实因信号退出。重启时不会复用原进程内存、证明或去重集合。
4. 第二个全新进程使用相同消费者用途、队列和订阅,在原交付真实租约到期后收到补发。它读取业务数据库的原Inbox,Duplicate=true,然后ACK新证明并正常退出。
5. 最终event_id、delivery_id保持不变,平台attempts=2并ACKED,业务Inbox仍1行、applied_events仍1。
测试屏障只存在于测试二进制;正常示例CLI不提供故障开关。已经提交的业务结果不因强杀而回滚,恢复依靠持久化Inbox,而非把新进程视为从未执行过。
## 6. 剩余边界与计划
- Inbox保证本例SQL事务内的原子去重与版本推进,不涵盖邮件、外部HTTP等业务副作用;需要相应幂等和接收方Outbox设计。
- 核心SDK仍只有网络协议依赖,业务表不进入平台迁移。正式接收方需要自己的DDL升级、权限、加密、容量、保留及备份恢复方案。
- 历史清理、凭据重绑、全量监控告警、MySQL故障切换和生产容量/RPO/RTO仍按P6实施。
- 已重新核对[原功能范围](../01-functional-spec.md)与[事务衔接要求](../04-reliability-and-data.md),将尚缺的批量受理、独立任务核查/人工重试、业务键/时间筛选与业务Outbox接入明确列回[持续计划](../08-development-plan.md)。P3及整个平台均未宣称最终完成。