feat: add reusable MySQL outbox and HTTP lifecycle example
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Orbit 持续开发与回归计划
|
||||
|
||||
更新:2026-09-22。此计划对应持续开发目标,由开发过程同步维护。数据库已按用户要求采用无外键设计,迁移 000008 移除历史外键,关联一致性由服务端事务保证。
|
||||
更新:2026-09-24。此计划对应持续开发目标,由开发过程同步维护。数据库已按用户要求采用无外键设计,迁移 000008 移除历史外键,关联一致性由服务端事务保证。
|
||||
|
||||
## 1. 最终交付目标
|
||||
|
||||
@@ -252,3 +252,10 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
|
||||
- 已验证:真实 MySQL 多节点竞争、落盘后确认丢失、文件损坏、策略变化、预算提高与卷预算、分页;标准测试、容器端到端、页面实际操作与全量集成回归均已通过。
|
||||
- 不计入本批完成项:源数据删除、跨作业增量去重、对象存储、任意网络文件系统语义及目标生产跨故障域验收。
|
||||
- [接口/部署/恢复示例](api/archive-scheduler.md)与[验证记录](reports/phase-6-archive-scheduler.md)。
|
||||
|
||||
### 可复用 Outbox 与 HTTP 服务生命周期(本批已完成)
|
||||
|
||||
- 已交付独立 MySQL 适配模块,支持调用方 EnqueueTx、受理确认钩子和有界 Run;旧 CLI 改用公共包,原有表结构及命令兼容。
|
||||
- 已交付完整 HTTP 宿主:业务接口、发送器、Worker、持久 Inbox、健康检查、后台错误传播、有界关闭和数据库最后释放。
|
||||
- 已通过:标准竞态测试、vet/build、SDK隔离和文档链接;定向真实MySQL/平台回归与平台全量 integration 均退出0。包括事务回滚/重复/旧快照、确认回滚、提交回执丢失、强杀恢复和SIGTERM收尾。
|
||||
- [公共包接口](sdk/outbox-mysql.md)、[完整调用和故障示例](examples/http-service.md)、[验收报告](reports/outbox-http-integration.md)。本批不扩展到其他数据库/ORM、流程命令Outbox或源数据删除。
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
独立的流程场景另见[有界轮询](workflow-poll.md)与[证据恢复](workflow-recovery.md):前者区分请求重试与业务轮次,后者实际注入外部提交后HTTP断线,保留UNKNOWN历史和暂停门控。本机渠道是受控夹具,不能代替持久业务证据。
|
||||
|
||||
## 已可运行:嵌入现有 HTTP 服务
|
||||
|
||||
[完整 HTTP 工程与命令](http-service.md):业务接口同事务入队 → 后台发送 → 内嵌 Worker → WebSocket → Inbox,包含健康检查、后台错误传播、SIGTERM收尾和SIGKILL恢复。公共包接口见 [MySQL Outbox](../sdk/outbox-mysql.md)。
|
||||
|
||||
## 已可运行:业务事务与任务提交Outbox
|
||||
|
||||
- 独立模块:[task-outbox](../../examples/task-outbox/README.md),[完整前置、命令、Go方法及故障处理](task-outbox.md)。
|
||||
|
||||
129
docs/examples/http-service.md
Normal file
129
docs/examples/http-service.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# HTTP 服务完整接入示例
|
||||
|
||||
[源码工程](../../examples/http-service/README.md)将 SDK 的发送器、Worker、结果消费器放进业务 HTTP 进程。Orbit 平台仍然独立部署,业务表放在自己的 MySQL 数据库。
|
||||
|
||||
## 1. 场景与运行结果
|
||||
|
||||
客户端用固定 operation_id 提交一个对象引用。HTTP 事务同时写入业务行和待发请求,后台发送给 Orbit。Worker 对引用进行纯计算转换,结果通过可靠 WebSocket 订阅写入业务 Inbox,再由 SDK 确认交付。
|
||||
|
||||
接口返回 202 表示业务本地事务已提交。查询先看到 PENDING/SENDING,随后 ACCEPTED,最后收到 execution=SUCCEEDED 和转换结果。每个阶段以数据库记录为准,HTTP 请求不等待远端任务执行结束。
|
||||
|
||||
## 2. 准备配置
|
||||
|
||||
需要 Go 1.27.1、运行中的 Orbit 和 MySQL 8.4。创建一个独立的业务数据库,例如 orbit_http_demo,不使用 Orbit 平台库。由业务迁移账号初始化表,正常运行账号只授予所需业务表读写权限。
|
||||
|
||||
在仓库根目录创建示例专用应用/队列凭据,下面的 ORBIT_MYSQL_DSN 是**平台库**:
|
||||
|
||||
```sh
|
||||
umask 077
|
||||
mkdir -p .local
|
||||
export ORBIT_MYSQL_DSN='平台数据库DSN'
|
||||
go run ./cmd/orbit-admin bootstrap \
|
||||
--application http-demo --namespace default --environment development \
|
||||
--queue http-demo --worker-access --event-access \
|
||||
--output .local/http-demo.credentials.json
|
||||
```
|
||||
|
||||
该命令用于一次性准备专用队列;重启复用原凭据,不重新 bootstrap。凭据具有任务提交、队列读取、Worker 和事件所需权限,生产可按角色拆分身份。
|
||||
|
||||
```sh
|
||||
# 在仓库根目录保存凭据绝对路径,再进入示例。
|
||||
export ORBIT_CREDENTIALS_FILE="$PWD/.local/http-demo.credentials.json"
|
||||
export ORBIT_URL='http://127.0.0.1:8080'
|
||||
export APP_MYSQL_DSN='业务账号:密码@tcp(127.0.0.1:3306)/orbit_http_demo'
|
||||
export APP_HTTP_ADDR='127.0.0.1:8090'
|
||||
cd examples/http-service
|
||||
export GOWORK=off
|
||||
|
||||
go run ./cmd/server init
|
||||
export ORBIT_SUBSCRIPTION_ID="$(go run ./cmd/server provision)"
|
||||
go build -o ./server ./cmd/server
|
||||
./server serve
|
||||
```
|
||||
|
||||
`init` 仅首次在空业务库执行,不自动升级或覆盖已有表;生产迁移管理四张表:sender_outbox、sender_jobs、receiver_inbox、receiver_task_views。全部字段有中文注释,没有外键。
|
||||
|
||||
`provision` 在发送任何业务任务前执行,使用固定键和固定内容创建订阅。将输出的订阅 ID 保存在部署配置中,后续启动只需 `serve`,不依赖 Orbit 在线才能接收本地业务请求。运行凭据必须能消费该订阅的绑定身份,轮换按原凭据规则执行。
|
||||
|
||||
也可用 ORBIT_TOKEN、ORBIT_NAMESPACE_ID、ORBIT_QUEUE_ID 配置代替文件;环境变量优先。APP_HTTP_ADDR 仅接受回环 IP,示例不提供对公网的账号体系。集成到现有 HTTP 服务时复用已有认证与授权中间件,不要直接取消绑定限制暴露演示路由。
|
||||
|
||||
## 3. HTTP 接口与调用
|
||||
|
||||
| 接口 | 返回 |
|
||||
| --- | --- |
|
||||
| `GET /health/live` | 进程存活:200 |
|
||||
| `GET /health/ready` | 正在接收请求且业务数据库可用:200;关闭期间/数据库不可用:503 |
|
||||
| `POST /jobs` | 保存业务事务,202;字段错误400、原操作内容冲突409、存储/提交不明503 |
|
||||
| `GET /jobs/{operation_id}` | 业务提交与结果视图200;不存在404、存储错误503 |
|
||||
|
||||
```sh
|
||||
export OPERATION_ID="$(openssl rand -hex 16)"
|
||||
curl -sS http://127.0.0.1:8090/jobs \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"operation_id\":\"$OPERATION_ID\",\"source_ref\":\"objects/demo.txt\"}"
|
||||
|
||||
curl -sS "http://127.0.0.1:8090/jobs/$OPERATION_ID"
|
||||
```
|
||||
|
||||
首次提交:
|
||||
|
||||
```json
|
||||
{"operation_id":"原操作ID","local_committed":true,"duplicate":false}
|
||||
```
|
||||
|
||||
等待结果入库后查询:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id":"原操作ID",
|
||||
"submission":"ACCEPTED",
|
||||
"code":"ACCEPTED",
|
||||
"task_id":"平台任务ID",
|
||||
"execution":"SUCCEEDED",
|
||||
"result":{"source_ref":"objects/demo.txt","normalized_ref":"OBJECTS/DEMO.TXT"}
|
||||
}
|
||||
```
|
||||
|
||||
用同一个 OPERATION_ID 和相同请求重复 POST,返回 duplicate=true,不重复写业务行或创建平台任务。相同 ID 改 source_ref 返回409。客户端超时/503时保留原 ID、原内容重试,不重新生成随机 ID。source_ref 最多512个Unicode字符,不能空白或包含控制字符;接口正文上限4KiB。
|
||||
|
||||
## 4. 生命周期
|
||||
|
||||
[生命周期源码](../../examples/http-service/cmd/server/lifecycle.go)包含以下顺序:
|
||||
|
||||
1. 加载配置、连接业务库、核验表、构造 SDK 组件,再创建 HTTP 监听。任何构造/监听失败都关闭已经持有的资源;不会先启动失去宿主管理的 Worker。
|
||||
2. 启动 HTTP、发送器、Worker 和结果消费器;各组件使用独立的宿主 context,进程信号只触发统一关闭流程。
|
||||
3. HTTP 监听失败或任何后台组件不可恢复退出,撤销 readiness,进入同一关闭路径,错误返回 main 并以非零状态退出。
|
||||
4. 收到 SIGTERM/SIGINT 后撤销 readiness,用**新的有期限 context**调用 HTTP Shutdown,等待已进入的业务请求完成;共享数据库及后台组件此时仍可用。
|
||||
5. HTTP 结束后取消发送器和 Worker,等待当前发送确认、在途任务续租及结果上报完成。
|
||||
6. 最后取消结果消费器,等待处理器/ACK 有界收尾,随后关闭共享数据库连接池。
|
||||
|
||||
发送器和 Worker 收尾各20s,结果消费器10s;示例每个关闭阶段最大25s,进程平台的终止宽限应大于三个阶段之和,例如90s。超时返回错误,不声称工作全部完成。忽略 context 的自定义处理器不能被 Go 强杀;实际业务必须遵守取消,并为外部副作用建立幂等/查询核对。
|
||||
|
||||
消费器取消会取消其处理器;事务未确认时不会 ACK,重启后允许重投。关闭前并不要求全局订阅积压归零,进程退出后未消费结果仍由持久订阅保存。示例 Inbox 按事件 ID 去重,按任务版本防止迟到覆盖,提交回执未知返回错误,不能提前确认交付。
|
||||
|
||||
readiness 表示能安全接受**本地持久请求**,不要求 Orbit 或 WebSocket 始终在线;Orbit暂时不可用时任务留在 Outbox。若某类业务必须同步依赖远端,应单独增加路由级就绪门槛。监控仍需覆盖 Outbox 积压/最老待发年龄/REVIEW、Worker重连和事件积压,不能只看健康接口。
|
||||
|
||||
## 5. 故障场景
|
||||
|
||||
| 场景 | 预期行为 |
|
||||
| --- | --- |
|
||||
| Orbit 暂时不可用 | HTTP仍可保存业务事务,Outbox有界退避;受理超出24h/100次进入REVIEW |
|
||||
| HTTP提交响应丢失 | 客户端按原ID与内容核对,业务行与待发请求不重复 |
|
||||
| Orbit受理响应丢失 | 发送器保留原提交键重新核对,不能换键 |
|
||||
| 本地业务/确认COMMIT不明 | 保留原身份,不把失败返回当成数据库回滚证明 |
|
||||
| 进程SIGKILL | 内存丢失;重启用相同数据库、目标、凭据和订阅ID恢复,已有发送租约自然到期 |
|
||||
| 正常SIGTERM | 按上述顺序收尾;成功退出码0,异常/超时非零 |
|
||||
| 结果重复投递 | 同业务事务Inbox去重,提交完成后才ACK |
|
||||
|
||||
不要把示例的纯计算 `RecoveryIdempotent` 直接用于支付、发送通知等外部效果。真实业务要实现自己的幂等或查询核对。任务预算、24h发送期限、结果交付预算保持有限,长时间停机可能需要人工核查。
|
||||
|
||||
## 6. 验证与接入现有框架
|
||||
|
||||
```sh
|
||||
# 仓库根目录:脚本自行创建、销毁专用MySQL,不操作本地开发库。
|
||||
make adapters-check
|
||||
make test vet build docs-check sdk-check
|
||||
make integration
|
||||
```
|
||||
|
||||
[验证记录](../reports/outbox-http-integration.md)区分纯生命周期测试和真实数据库/平台/进程证据。Gin、Echo、go-zero 等服务可复用本例的组件装配和关闭顺序,将业务路由挂到原路由器,统一接入原生命周期;不需要另起一套业务 HTTP 服务,也不在每次请求里创建 Worker。
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 1. 数据与状态
|
||||
|
||||
[中文模型](../../examples/task-outbox/internal/outbox/model.go)和[SQL](../../examples/task-outbox/internal/outbox/schema.sql)包含2张业务表、22个字段,全部有中文说明,外键为零,不进入Orbit平台迁移。
|
||||
[中文模型](../../examples/task-outbox/internal/outbox/model.go)和[SQL](../../examples/task-outbox/internal/outbox/schema.sql)与[公共 Outbox SQL](../../adapters/outbox/mysql/schema.sql)合计包含2张业务表、22个字段,全部有中文说明,外键为零,不进入Orbit平台迁移。
|
||||
|
||||
| 表 | 职责与关键约束 |
|
||||
| --- | --- |
|
||||
@@ -103,7 +103,7 @@ GOWORK=off go run ./cmd/sender status "$SENDER_OPERATION_ID"
|
||||
|
||||
## 4. Go接入接口
|
||||
|
||||
以下方法属于示例的内部业务适配包,完整源码见[store.go](../../examples/task-outbox/internal/outbox/store.go)及[sender.go](../../examples/task-outbox/internal/outbox/sender.go)。接入自己的业务模块时保留事务顺序与协议,替换sender_jobs为实际业务表;核心Orbit客户端仍使用独立SDK。
|
||||
发送与租约实现已抽至[公共适配包](../sdk/outbox-mysql.md)。以下方法是本示例的业务封装,完整源码见[store.go](../../examples/task-outbox/internal/outbox/store.go)及[sender.go](../../adapters/outbox/mysql/sender.go)。接入自己的业务模块时保留事务顺序与协议,替换sender_jobs为实际业务表;核心Orbit客户端仍使用独立SDK。
|
||||
|
||||
```go
|
||||
func Open(ctx context.Context, dsn string) (*sql.DB, error)
|
||||
|
||||
44
docs/reports/outbox-http-integration.md
Normal file
44
docs/reports/outbox-http-integration.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 可复用 Outbox 与 HTTP 生命周期验证
|
||||
|
||||
日期:2026-09-24。本报告仅覆盖本批接入适配与示例,不替代平台目标生产环境容量、高可用及数据库复制确认验收。
|
||||
|
||||
## 交付
|
||||
|
||||
- 独立模块 adapters/outbox/mysql,公开 EnqueueTx、可选受理关联回调、Run 和查询接口。无平台内部依赖,核心 SDK 无 SQL 驱动。
|
||||
- 旧 CLI 的发送核心及原有故障测试迁入公共模块;旧业务表、命令和固定提交身份兼容。
|
||||
- examples/http-service 提供完整装配、业务路由、数据库事务、Worker、持久 Inbox、后台错误传播及有界关闭。
|
||||
- 业务库与平台库分离;SQL沿用无外键表、中文字段注释。平台迁移及SDK协议未修改。
|
||||
|
||||
## 验收场景
|
||||
|
||||
1. 业务回滚同时撤销待发记录;未提交的记录不能领取;调用方负责 Commit。
|
||||
2. 8 个并发重复、同事务重复、RR 旧快照核对;内容/目标冲突拒绝;大整数保真。
|
||||
3. 接受钩子失败整笔确认回滚;无钩子也可确认,不依赖示例业务表;旧租约代次不能覆盖新确认。
|
||||
4. 原HTTP回执丢失、三个阶段的真实MySQL COMMIT成功包丢失、平台受理后发送器SIGKILL及自然租约接管。
|
||||
5. Run取消时等待在途发送完成确认,超时返回错误并保留持久恢复状态。
|
||||
6. HTTP请求未结束时后台依赖保持可用;后台致命错误触发宿主关闭;消费器在发送/执行组件之后结束。
|
||||
7. 完整进程:代理令Orbit不可用,HTTP仍提交本地事务;SIGKILL旧宿主并确认退出;新进程恢复原操作,实际Worker执行、Inbox入库、平台ACK;重复HTTP不新建业务行,改内容409,SIGTERM退出0。
|
||||
|
||||
## 验证状态
|
||||
|
||||
环境:macOS arm64、Go 1.27.1、Docker隔离MySQL 8.4、独立业务库与Orbit平台进程,回环地址;业务与平台使用同一隔离MySQL实例,不代表跨故障域部署。
|
||||
|
||||
- `make test`:六个模块的标准竞态测试通过(未配置数据库时Integration会跳过,真实数据库证据另列)。
|
||||
- `make vet build sdk-check`:通过;SDK 30个包、禁止依赖0项。适配器在GOWORK=off下使用已发布SDK v0.1.0,依赖图无平台内部包。
|
||||
- `make docs-check`:166份Markdown的本地链接检查通过。
|
||||
- `make adapters-check`:隔离MySQL/平台/业务模块定向回归退出0;公共Outbox 33.792s,完整HTTP进程场景9.52s,全部通过。恢复测试真实等待旧发送租约自然到期。
|
||||
- `make integration`:全量平台回归退出0;包含真实存储/升级、SDK、旧CLI、公共Outbox、HTTP宿主及Worker/Event/Workflow进程恢复。全量链路中Outbox 33.934s,HTTP进程场景9.64s,新增同事务重复与RR核对均通过。
|
||||
|
||||
复现:`make adapters-check`(定向隔离链路)和 `make integration`(平台全量隔离链路)。不操作现有开发数据库或运行容器。
|
||||
|
||||
## 本批发现与修正
|
||||
|
||||
新增发送器关闭测试的初版 HTTP 桩缺失协议必填 request_id,SDK正确拒绝无效响应,使测试未进入提交阶段。补齐符合正式协议的响应后重新执行。随后发现超时路径的测试桩仍持有未释放屏障,导致 httptest.Close 等待;已在清理中显式、幂等释放屏障并重跑。两次未通过运行均不计入通过证据。
|
||||
|
||||
## 边界
|
||||
|
||||
- 首版适配器仅支持 MySQL、SubmitTask;不会自动写业务表、删除历史或自动解除 REVIEW。
|
||||
- 外部副作用仍需业务幂等/核对;示例 Worker 仅做可重复纯计算。
|
||||
- 每条记录沿用24小时/100次总发送预算,重启不延长。ACCEPTED仅为平台受理。
|
||||
- HTTP示例只监听回环地址;接入真实服务复用业务自身鉴权、监控和部署生命周期。健康就绪不等于全局任务已经执行完毕。
|
||||
- Inbox是示例业务实现,不是本批新增的公共适配包;其他数据库/ORM专用集成不在本批交付内。
|
||||
@@ -7,13 +7,13 @@
|
||||
- [独立模块](../../examples/task-outbox/README.md)只依赖独立Orbit SDK与业务侧MySQL驱动,核心SDK没有新增依赖。
|
||||
- [模型](../../examples/task-outbox/internal/outbox/model.go)与[建表](../../examples/task-outbox/internal/outbox/schema.sql):2个业务实体、22个Go/SQL中文注释字段,零外键;不改变平台35个实体和289个持久化字段。
|
||||
- [业务事务](../../examples/task-outbox/internal/outbox/store.go):业务操作与待发命令原子创建,稳定操作/提交身份、冻结请求、摘要、有限次数、租约代次及原回执;确认与业务任务关联再次同事务。
|
||||
- [发送器](../../examples/task-outbox/internal/outbox/sender.go):事务外HTTP、当前凭据核对目标、原键重报、背压退避、未知结果分类、过期接管与旧代次拒绝。
|
||||
- [发送器](../../adapters/outbox/mysql/sender.go):事务外HTTP、当前凭据核对目标、原键重报、背压退避、未知结果分类、过期接管与旧代次拒绝。
|
||||
- [CLI](../../examples/task-outbox/cmd/sender/main.go)及[完整命令/方法/恢复手册](../examples/task-outbox.md):init、enqueue、send、status和demo。
|
||||
- [Makefile](../../Makefile)和[隔离脚本](../../scripts/test-mysql.sh)纳入独立模块,使用单独业务数据库和专用平台凭据,不触碰现有开发数据库。
|
||||
|
||||
## 2. 定向数据库与故障证据
|
||||
|
||||
[数据库回归](../../examples/task-outbox/internal/outbox/store_integration_test.go)验证:
|
||||
[数据库回归](../../adapters/outbox/mysql/store_integration_test.go)验证:
|
||||
|
||||
1. 8个相同业务操作并发入库只有一条业务记录/Outbox,7次重复核对;修改正文、源引用或目标队列不能复用原操作。
|
||||
2. 实际业务INSERT失败时Outbox一起回滚;业务关联UPDATE失败时ACCEPTED一起回滚,恢复后可保存原回执。
|
||||
@@ -21,13 +21,13 @@
|
||||
4. 错误目标、损坏文档、权限拒绝、幂等冲突和无效回执停止自动发送;背压遵守Retry-After,次数耗尽和截止产生REVIEW,最后一次未知响应不能绕过上限。
|
||||
5. 实际Schema查询确认22个中文列注释、外键0,通过实际违规写入验证JSON、状态、次数和截止CHECK生效,领取查询使用ix_sender_due且无filesort;大整数9007199254740993在冻结请求与真实回执中保持精确。
|
||||
|
||||
[COMMIT故障代理](../../examples/task-outbox/internal/outbox/commit_loss_integration_test.go)在业务入库、领取和本地确认三处分别截获真实MySQL COMMIT OK包后丢弃。调用方得到ErrCommitUnconfirmed,新Store分别核对已存在业务记录、已提交租约和已确认关联,不假定COMMIT失败即回滚。
|
||||
[COMMIT故障代理](../../adapters/outbox/mysql/commit_loss_integration_test.go)在业务入库、领取和本地确认三处分别截获真实MySQL COMMIT OK包后丢弃。调用方得到ErrCommitUnconfirmed,新Store分别核对已存在业务记录、已提交租约和已确认关联,不假定COMMIT失败即回滚。
|
||||
|
||||
[HTTP回执丢失](../../examples/task-outbox/internal/outbox/http_integration_test.go)先取得真实平台受理结果,再向客户端发送不完整响应并断开;Outbox保持PENDING,原键重报后同一任务ACCEPTED、deduplicated=true、发送次数2,队列只有一项任务。错误持久化命名空间在提交前被拒绝。
|
||||
[HTTP回执丢失](../../adapters/outbox/mysql/http_integration_test.go)先取得真实平台受理结果,再向客户端发送不完整响应并断开;Outbox保持PENDING,原键重报后同一任务ACCEPTED、deduplicated=true、发送次数2,队列只有一项任务。错误持久化命名空间在提交前被拒绝。
|
||||
|
||||
[发送进程故障](../../examples/task-outbox/internal/outbox/process_integration_test.go)等待真实平台受理后、本地确认前的屏障,再SIGKILL并确认旧进程已退出。新进程等待数据库30秒租约自然到期,以原键取得原回执;最终只存在一个平台任务和一条本地业务记录。测试没有直接改时间来替代这项进程接管。
|
||||
[发送进程故障](../../adapters/outbox/mysql/process_integration_test.go)等待真实平台受理后、本地确认前的屏障,再SIGKILL并确认旧进程已退出。新进程等待数据库30秒租约自然到期,以原键取得原回执;最终只存在一个平台任务和一条本地业务记录。测试没有直接改时间来替代这项进程接管。
|
||||
|
||||
定向脚本第三轮正常退出,状态码0:Outbox集成33.009秒,其中发送进程故障31.19秒;init/demo均正常完成。随后增加的[真实CLI测试](../../examples/task-outbox/internal/outbox/cli_integration_test.go)进入本轮全量回归,验证编译后的enqueue重复、status、send及正常信号停止。
|
||||
定向脚本第三轮正常退出,状态码0:Outbox集成33.009秒,其中发送进程故障31.19秒;init/demo均正常完成。随后增加的[真实CLI测试](../../adapters/outbox/mysql/cli_integration_test.go)进入本轮全量回归,验证编译后的enqueue重复、status、send及正常信号停止。
|
||||
|
||||
## 3. 发现与修正
|
||||
|
||||
@@ -73,3 +73,5 @@ make integration
|
||||
- REVIEW保留原数据,由接入方带证据和审计的管理流程处置;本示例未实现通用业务审核系统或跨业务ORM适配。
|
||||
- 核心SDK、平台Schema和执行协议均未改变;新增业务示例不是已发布的兼容性SDK包。
|
||||
- 流程启动与信号Outbox类型需随Weave API实现;P6继续容量、数据库切换、备份恢复及运维验证。当前正确性与进程恢复证据不等同于生产高可用或百万规模容量结论。
|
||||
|
||||
2026-09-24 源码组织调整:发送核心及原故障回归迁至独立 adapters/outbox/mysql 模块,业务示例调用公共包;Outbox Record/SQL见[公共模型](../../adapters/outbox/mysql/model.go)与[表结构](../../adapters/outbox/mysql/schema.sql),sender_jobs 保留在旧业务示例。上述历史验证日期不变,新增验证另见[接入适配报告](outbox-http-integration.md)。
|
||||
|
||||
@@ -88,7 +88,7 @@ fmt.Println(current.ID, current.State, current.Revision)
|
||||
|
||||
受理回执不是业务执行结果。同键重报返回原始快照;当前状态应使用 GetTask。业务侧创建操作与平台提交之间存在双写边界,接入方应在自身事务中保存操作记录/Outbox,再使用稳定身份提交,不能依靠随机新键补发来“修复”未知结果。
|
||||
|
||||
[业务事务Outbox完整场景](../examples/task-outbox.md)已提供独立业务数据库、原子业务写入、持久化发送器、原身份恢复及CLI;核心SDK保持无MySQL/ORM依赖。业务适配包的Enqueue/SendOnce/Status与实际运行命令均在该手册说明。
|
||||
[业务事务Outbox完整场景](../examples/task-outbox.md)已提供独立业务数据库、原子业务写入、持久化发送器、原身份恢复及CLI;核心SDK保持无MySQL/ORM依赖。现已抽为[可直接导入的 MySQL 适配包](outbox-mysql.md),支持 EnqueueTx/SendOnce/Run/Status 和受理确认钩子;[完整 HTTP 服务示例](../examples/http-service.md)演示宿主生命周期与结果入库。
|
||||
|
||||
## 3. 取消与错误处理
|
||||
|
||||
|
||||
123
docs/sdk/outbox-mysql.md
Normal file
123
docs/sdk/outbox-mysql.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# MySQL 事务 Outbox 适配包
|
||||
|
||||
## 1. 用途与安装
|
||||
|
||||
业务写库与提交 Orbit 是两次独立操作。Outbox 将业务变更和待发请求放在同一个业务数据库事务中,再通过后台发送器提交任务。提交响应丢失时复用原操作身份,避免业务记录已保存而任务丢失。
|
||||
|
||||
```sh
|
||||
go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@codex/orbit-foundation
|
||||
```
|
||||
|
||||
Go 会将分支解析为固定版本写入 go.mod。上线应固定已验证的版本,不在每次部署时追踪分支。本包要求 Go 1.27.1,使用 SDK v0.1.0 和 MySQL 8.4 验证;不向核心 SDK 加入 SQL/ORM 依赖,也不依赖 Orbit 服务端模型。其他数据库和 ORM 专用适配尚未提供。
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
outbox "git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql"
|
||||
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||
)
|
||||
```
|
||||
|
||||
完整可编译工程在 [HTTP 服务示例](../../examples/http-service/README.md);[旧 CLI 示例](../examples/task-outbox.md)现在也调用本包,不维护第二份发送实现。
|
||||
|
||||
## 2. 数据库与事务约束
|
||||
|
||||
在**业务数据库**应用 [schema.sql](../../adapters/outbox/mysql/schema.sql)。表名保留 `sender_outbox`,兼容原示例已经初始化的表。中文模型见 [Record](../../adapters/outbox/mysql/model.go)。没有外键,不执行数据删除,不属于 Orbit 平台迁移。
|
||||
|
||||
- `New` 不建表、不连接网络、不接管连接池关闭;显式 `Init` 只创建新表,遇到已有表报错。
|
||||
- 生产由业务迁移工具管理 `SchemaSQL()` 的内容;不要在每次启动时执行 Init。
|
||||
- `db` 和传入 `tx` 必须来自同一个业务库、同一个主写端;不支持跨库事务或读副本确认。
|
||||
- 自行创建连接池时配置 `parseTime=true`、UTC 会话时区和合理连接/读写超时;`Open(ctx, dsn)` 是可选的已配置连接池入口。
|
||||
- 事务锁序统一为 **Outbox → 业务行**。先调用 EnqueueTx,再写业务行;受理回调遵守同样顺序。不要持有业务行锁反向等待 Outbox。
|
||||
- 业务操作 ID 必须由调用方在首次请求前生成并保存,格式为 32 位小写十六进制;同命名空间唯一。HTTP 调用方丢响应后使用原 ID 与原内容重试。
|
||||
|
||||
## 3. 公共方法
|
||||
|
||||
| 方法 | 职责 |
|
||||
| --- | --- |
|
||||
| `Open(ctx, dsn) (*sql.DB, error)` | 可选的业务库连接池构造;不会自动建表 |
|
||||
| `SchemaSQL() string` | 返回初始表结构供业务迁移管理 |
|
||||
| `Init(ctx, db) error` | 显式创建新表,仅用于首次初始化 |
|
||||
| `New(db, namespaceID, queueID, Options) (*Store, error)` | 固定目标及可选受理回调,可并发复用 |
|
||||
| `EnqueueTx(ctx, tx, operationID, SubmitTaskRequest) (duplicate, error)` | 在调用方事务写入待发任务;不 Commit/Rollback、不发 HTTP |
|
||||
| `SendOnce(ctx, client) (Record, error)` | 有界领取并处理一条到期记录;无到期项返回 ErrIdle |
|
||||
| `Run(ctx, client, RunOptions) error` | 持续发送;取消后停止领取,限时收尾当前发送 |
|
||||
| `Status(ctx, operationID) (Record, error)` | 只读固定命名空间及队列中的本地提交状态 |
|
||||
|
||||
基本构造不要求业务回调:
|
||||
|
||||
```go
|
||||
sender, err := outbox.New(db, namespaceID, queueID, outbox.Options{})
|
||||
if err != nil { return err }
|
||||
```
|
||||
|
||||
`Options.OnAccepted` 类型为:
|
||||
|
||||
```go
|
||||
type AcceptedHook func(context.Context, *sql.Tx, Record, orbit.SubmitTaskResult) error
|
||||
```
|
||||
|
||||
回调与 Outbox 的 ACCEPTED 更新在同一个短事务内执行,并核对最新领取代次。可在此将平台 TaskID 关联到业务记录。回调必须幂等、响应 context,只做同库 SQL,不能 Commit/Rollback 或调用外部服务;失败整笔确认回滚,返回 `ErrAcceptance`。回调收到的 Record 仍是被锁定的 SENDING 记录,受理 TaskID 从 receipt 读取。不开启回调时,任务关联保存在 Outbox 自身,业务可按 OperationID 查询。
|
||||
|
||||
## 4. 加入现有业务事务
|
||||
|
||||
下面是接入函数片段。完整业务重复核对与错误处理见 [jobs/store.go](../../examples/http-service/internal/jobs/store.go)。
|
||||
|
||||
```go
|
||||
func save(ctx context.Context, db *sql.DB, sender *outbox.Store,
|
||||
operationID string, request orbit.SubmitTaskRequest) error {
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||
if err != nil { return err }
|
||||
defer tx.Rollback()
|
||||
|
||||
duplicate, err := sender.EnqueueTx(ctx, tx, operationID, request)
|
||||
if err != nil { return err } // 必须回滚整笔事务
|
||||
if !duplicate {
|
||||
// 在这里使用 tx 写入自己的业务表。
|
||||
// 每一个 SQL 错误都必须返回;禁止忽略错误后提交。
|
||||
} else {
|
||||
// 核对原业务记录与本次输入一致,不能再次执行扣款、计数等操作。
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return outbox.ErrCommitUnconfirmed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
EnqueueTx 返回 nil 只代表该事务内写入/核对完成。事务提交前另一发送器看不到该记录;调用方回滚会同时撤销待发任务与业务写入。同一事务重复入队返回 duplicate=true,但仍需由调用方确认 Commit。默认 REPEATABLE READ 下的重复核对使用共享锁当前读,不依赖旧快照,也不升级重复 INSERT 持有的共享锁。
|
||||
|
||||
COMMIT 错误不能解释成“肯定没保存”。保留原 ID 与内容,重做同一业务事务核对;不得生成新 ID 补发。冻结请求按字节摘要核对,目标、提交键、默认值和数字精度保持不变。
|
||||
|
||||
## 5. 发送器与生命周期
|
||||
|
||||
```go
|
||||
err := sender.Run(workerContext, client, outbox.RunOptions{
|
||||
PollInterval: 500 * time.Millisecond,
|
||||
ShutdownTimeout: 20 * time.Second,
|
||||
})
|
||||
```
|
||||
|
||||
RunOptions 默认轮询间隔 500ms(10ms–10s),关闭期限默认 20s(1ms–5min)。HTTP 在数据库事务外,单次网络阶段最多 15s、发送请求最多 10s;领取租约 30s,SQL 确认最多 5s。数据库时间控制首次入库后 24h、最多 100 次领取的总预算;进程重启不重置预算。
|
||||
|
||||
Run context 取消后停止新领取,在关闭期限内等待当前交换与本地确认,宿主必须等待 Run 返回。`ErrShutdownTimeout` 表示有未核对的持久记录,应在重启后沿原身份恢复。HTTP 暂时失败在库内退避;数据库错误或受理回调失败会让 Run 返回错误,由宿主降低就绪状态、关闭或按自身策略恢复。REVIEW 记录停止自动发送,需要监控 Status/数据库并人工核查。
|
||||
|
||||
允许多个 Run/进程共用同一范围,由 MySQL SKIP LOCKED 和递增代次协调。网络请求可能重复,平台使用原幂等键去重;不承诺业务外部效果恰好一次。
|
||||
|
||||
## 6. 状态与错误
|
||||
|
||||
| 状态/错误 | 含义及处理 |
|
||||
| --- | --- |
|
||||
| PENDING / SENDING | 未完成本地受理确认;不能据此认定 Orbit 没收到 |
|
||||
| ACCEPTED | 已确认平台受理,TaskID/ReceiptJSON 可用;不是执行成功 |
|
||||
| REVIEW | 权限、内容冲突、无效回执或预算耗尽;保留证据人工核对 |
|
||||
| ErrConflict | 原操作的目标或内容不一致,不能覆盖 |
|
||||
| ErrCommitUnconfirmed | 领取/确认 COMMIT 回执不明;保留原身份核对 |
|
||||
| ErrLeaseLost | 领取代次已替换,旧发送器不得覆盖新结果;Run 可继续其他项 |
|
||||
| ErrAcceptance | 本地受理关联回调失败,确认事务未获成功确认 |
|
||||
| ErrStorage | 存储失败;当前进度以持久记录为准 |
|
||||
| ErrIdle / ErrNotFound | 无到期记录 / 当前范围没有该操作 |
|
||||
|
||||
本版本只支持 SubmitTask,不提供流程启动/信号 Outbox 命令,不自动删除历史、重置发送预算或解除 REVIEW。任务完成另用可靠订阅与业务 Inbox,详见 [完整 HTTP 示例](../examples/http-service.md)。
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
这份手册把 SDK、HTTP 查询和 WebSocket 结果通知串成一条可落地的接入链路。它只描述平台协议,不包含任何业务系统名称、行业规则或外部副作用。
|
||||
|
||||
完整可运行的业务宿主见[HTTP 服务示例](../examples/http-service.md),可复用事务能力见[MySQL Outbox 包](outbox-mysql.md)。
|
||||
|
||||
## 1. 接入边界
|
||||
|
||||
业务服务保存操作身份和请求正文,再调用 SDK 提交。`Idempotency-Key` 必须由业务库或 Outbox 持久化,网络超时、进程重启和响应不明时复用原键;不能生成新键猜测服务端是否已提交。受理回执只表示平台已经保存请求,不能当作任务或流程完成。
|
||||
@@ -24,19 +26,19 @@ taskID := receipt.Task.ID
|
||||
|
||||
## 2. 完成通知与权威查询
|
||||
|
||||
结果通知使用 Event WebSocket。连接时声明 `orbit.events.v1` 子协议和允许的来源,SDK 自动发送心跳、在连接断开后重连,并把上一次未确认的投递证明带回新连接。处理器完成业务事务后再调用 `Ack`;处理器失败或进程在提交前退出时不要确认,平台会在租约到期后重投同一交付。
|
||||
结果通知使用 Event WebSocket。连接时声明 `orbit.events.v1` 子协议和允许的来源,SDK 自动发送心跳、在连接断开后重连,并把上一次未确认的投递证明带回新连接。自动消费器的处理器完成业务事务后返回 nil,SDK 再发送 ACK;处理器失败或进程在提交前退出时不要确认,平台会在租约到期后重投同一交付。
|
||||
|
||||
```go
|
||||
consumer := orbit.NewEventConsumer(api, orbit.EventConsumerOptions{
|
||||
consumer, err := orbit.NewEventConsumer(api, orbit.EventConsumerOptions{
|
||||
SubscriptionID: subscriptionID,
|
||||
MaxHandlers: 8,
|
||||
})
|
||||
err := consumer.Run(ctx, func(ctx context.Context, delivery orbit.EventDelivery) error {
|
||||
// 先在业务 Inbox 中按 delivery.EventID 去重,再提交业务事务。
|
||||
// 返回 nil 后 SDK 才发送 ACK;返回错误会保留交付等待重投。
|
||||
return applyOnce(ctx, delivery)
|
||||
Capacity: 8,
|
||||
Handle: func(ctx context.Context, event orbit.ResultEvent) error {
|
||||
// 先按 event.ID 持久化去重,去重与业务更新在同一事务确认后返回 nil。
|
||||
return applyOnce(ctx, event)
|
||||
},
|
||||
})
|
||||
if err != nil { return err }
|
||||
if err := consumer.Run(ctx); err != nil { return err }
|
||||
```
|
||||
|
||||
通知只推动业务侧再次读取权威状态。收到 `task.completed`、`workflow.stopped` 或 `workflow.review_required` 后,调用 `GetTask`、`GetWorkflow` 或对应历史接口;事件快照不替代当前状态,也不表示外部效果已经完成。没有 WebSocket 时可以使用有界 HTTP 查询作为兜底,但必须使用退避、截止时间和业务键,不得无限轮询。
|
||||
|
||||
Reference in New Issue
Block a user