feat: add reusable MySQL outbox and HTTP lifecycle example
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user