Files
orbit/docs/examples/task-outbox.md

177 lines
12 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.
# 业务事务与任务提交 Outbox
完整项目:[task-outbox](../../examples/task-outbox/README.md)。它演示接入方保存对象处理请求的业务记录,同时保存向Orbit提交任务的待发命令。平台受理后,再在业务库里同时确认Outbox与任务关联。核心SDK继续独立,不依赖MySQL或ORM。
## 1. 数据与状态
[中文模型](../../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平台迁移。
| 表 | 职责与关键约束 |
| --- | --- |
| sender_jobs | 业务操作、对象引用及已确认任务ID;主键(namespace_id,operation_id) |
| sender_outbox | 固定命名空间/队列、原提交键、原请求、SHA256、发送状态、次数/代次、截止及回执;主键与业务操作相同 |
operation_id由调用方先持久化,再作为参数传入,为32位小写十六进制ID。source_ref要求有效UTF-8、非全空白、最多512个Unicode字符且不含控制字符。提交键固定为`outbox-task-<operation_id>`,同命名空间内唯一。业务源引用、目标队列或正文不同的重复操作返回ErrConflict;相同对象可以有不同业务操作,幂等范围由业务操作语义决定。
```mermaid
flowchart LR
B[业务记录与待发命令同事务] --> P[PENDING]
P -->|领取并提交租约| S[SENDING]
S -->|平台受理回执| A[ACCEPTED与业务关联同事务]
S -->|未知响应或背压| P
S -->|租约过期后原身份接管| S
S -->|内容、权限或预算需核查| R[REVIEW]
P -->|发送截止或次数耗尽| R
```
| 本地状态 | 含义 |
| --- | --- |
| PENDING | 待下一次有限发送;可能曾有未确认的平台受理 |
| SENDING | 已领取30秒租约;HTTP在事务外,进程退出后可按数据库时间接管 |
| ACCEPTED | 原平台回执和业务任务关联均已提交;任务执行结果另行GetTask或订阅获取 |
| REVIEW | 自动发送停止,保留原身份、请求及稳定错误分类供核查 |
TaskID为空只说明本地尚未确认。业务提交前不发HTTP,发送失败不回滚已提交业务记录。真正的对象处理仍由业务Worker完成;本例演示提交后取消,不调用外部对象服务。
## 2. 初始化与完整演示
需要Go1.27.1、MySQL8.4+及当前Orbit迁移/服务。分别配置平台库`ORBIT_MYSQL_DSN`和独立业务库`ORBIT_SENDER_DSN`,通过环境注入凭据,不把真实密码写入文档、命令历史或Git。
业务库由业务方先创建,示意SQL在自己的开发MySQL执行:
```sql
CREATE DATABASE orbit_sender_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
```
从仓库根目录初始化Orbit,输出凭据文件必须不存在:
```sh
go run ./cmd/orbit-migrate up
mkdir -p .local
chmod 700 .local
go run ./cmd/orbit-admin bootstrap \
--application outbox-demo --namespace default --environment development \
--queue outbox --output .local/outbox.credentials.json
go run ./cmd/orbit
```
另开终端,设置指向空业务库的ORBIT_SENDER_DSN后,从仓库根目录执行:
```sh
cd examples/task-outbox
export ORBIT_URL=http://127.0.0.1:8080
export ORBIT_CREDENTIALS_FILE=../../.local/outbox.credentials.json
GOWORK=off go run ./cmd/sender init
GOWORK=off go run ./cmd/sender demo
```
init只创建本模块两张表,已有表会报错。生产已有业务库应使用自身迁移,不直接运行此初始化。demo使用独立操作ID,先输出ID,再创建一条业务记录、重复核对、发送并取消受理任务。运行上限20秒,任务从平台受理起有60秒总时限;原历史和审计保留。
```text
sender_schema_initialized=true foreign_keys=0
demo_operation=<本次操作ID>
operation=<本次操作ID> state=ACCEPTED sends=1 code=ACCEPTED task=<任务ID>
local_duplicate=true accepted_once=true execution_success_not_implied=true
```
每次demo新建操作,失败后应保留打印的ID并查询已有记录,不能再次运行demo当成原操作恢复。演示队列保持专用且不启动object.prepare处理器,避免取消示例与真实业务执行竞争。
## 3. 分开提交、发送和恢复
以下命令沿用上面的环境。`SENDER_OPERATION_ID`应替换为业务系统已持久化的32位操作ID,重报继续使用同一个值。
```sh
export SENDER_OPERATION_ID=11111111111111111111111111111111
GOWORK=off go run ./cmd/sender enqueue "$SENDER_OPERATION_ID" objects/input.txt
GOWORK=off go run ./cmd/sender status "$SENDER_OPERATION_ID"
GOWORK=off go run ./cmd/sender send
```
enqueue只提交本地事务,不等待网络。首次输出local_committed=true、duplicate=false;同内容再次调用返回duplicate=true,即使Outbox后来已受理也不再次创建业务记录。输入固定handler=object.prepare、版本v1、business_key=操作ID,payload包含对象引用和精确整数9007199254740993。业务引用与完整请求在首次事务中冻结。
send持续处理当前命名空间与队列的待发项,无任务时每500毫秒等待;存储等错误后等待2秒。可运行多个发送进程;Ctrl-C/SIGTERM停止后,用同一环境重新启动send即可继续。数据库租约使用索引范围查询及SKIP LOCKED领取,不依赖进程内队列。
另开相同环境的终端查看:
```sh
GOWORK=off go run ./cmd/sender status "$SENDER_OPERATION_ID"
```
预期由PENDING/SENDING变成ACCEPTED,包含任务ID。运行演示失败、COMMIT未知、服务中断或进程重启后都先查看同一个ID。status只读,不触发发送或变更预算。REVIEW不自动恢复;本例没有重置预算、换键或强制确认命令。
凭据文件提供token、namespace_id、queue_id;ORBIT_TOKEN、ORBIT_NAMESPACE_ID、ORBIT_QUEUE_ID可覆盖对应字段。发送器需要queues:read、tasks:submit及固定队列范围;demo还需tasks:read和tasks:cancel。换凭据后发送前再次GetQueue核对命名空间与队列;配置的目标地址必须仍指向原Orbit部署,不能将已有Outbox切换到独立的新部署。
## 4. Go接入接口
发送与租约实现已抽至[公共适配包](../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)
func Init(ctx context.Context, db *sql.DB) error
func New(db *sql.DB, namespaceID, queueID string) (*Store, error)
func (s *Store) Enqueue(ctx context.Context, operationID, sourceRef string,
request orbit.SubmitTaskRequest) (duplicate bool, err error)
func (s *Store) SendOnce(ctx context.Context, api *orbit.Client) (Record, error)
func (s *Store) Status(ctx context.Context, operationID string) (Record, error)
```
已有db、命名空间、队列与客户端时,调用片段如下;完整imports/main/上下文和错误处理见[CLI源码](../../examples/task-outbox/cmd/sender/main.go)。
```go
store, err := outbox.New(db, namespaceID, queueID)
if err != nil { return err }
duplicate, err := store.Enqueue(ctx, operationID, "objects/input.txt",
orbit.SubmitTaskRequest{
QueueID: queueID, Handler: "object.prepare",
HandlerVersion: "v1", PayloadVersion: "v1",
Payload: json.RawMessage(`{"source_ref":"objects/input.txt"}`),
BusinessKey: operationID, TaskTimeoutMS: 60000,
})
if err != nil { return err }
fmt.Println("本地已提交", duplicate)
record, err := store.SendOnce(ctx, client)
if errors.Is(err, outbox.ErrIdle) { return nil }
if err != nil { return err }
fmt.Println(record.OperationID, record.State)
```
Enqueue先拒绝非法UTF-8字符串,避免JSON编码静默替换业务标识,再冻结当前v1默认时限、恢复等级、尝试策略和UTC时间;SHA256校验冻结文档的字节,不采用平台内部摘要算法。不对业务JSON对象键或数值表达做语义等价推断,因此同操作重报应保留原输入;完整平台字段规则仍由[任务API](../api/tasks.md)校验。升级业务适配层或SDK前应对已有待发文档做兼容回归,不能静默更新请求。
业务事务固定先锁Outbox操作身份,再写业务行;平台确认事务也先锁Outbox再写业务关联。适配已有业务事务时需统一全局锁顺序。所有网络交换均在这些事务之外,禁止先向平台提交再保存本地业务记录。
## 5. 预算、错误和不确定结果
| 规则 | 当前示例值与处理 |
| --- | --- |
| 单次本地事务 | 最多5秒;失败不盲目重放整个事务 |
| 领取租约 | 30秒,数据库UTC时间;每次领取send_count/lease_epoch同时递增 |
| 网络工作期限 | 从领取前开始最多15秒;提交HTTP再限制10秒及数据库剩余租约/截止 |
| 发送次数 | 最多100次领取,包含领取后未发HTTP便退出的情况 |
| 总发送期限 | 首次本地入库后24小时,短于平台90天幂等保留窗口;期满停止新发送 |
| 退避 | 指数1、2、4…最多60秒,加0–999毫秒抖动;429遵守有效Retry-After,整体不超过1小时 |
| 确认 | 只接受最近租约代次;尚未被替代的过期代次仍可保存真实受理回执 |
| 错误或分类 | 行为 |
| --- | --- |
| ErrConflict | 同业务操作内容不同;核查原记录,不换ID掩盖冲突 |
| ErrCommitUnconfirmed | 本地提交结果未知;查询或用原操作重报Enqueue,发送器按已提交状态继续 |
| ErrStorage | 本地存储失败;保留原身份并恢复连接,不能推断远端未受理 |
| ErrLeaseLost | 已有新代次或新状态;旧发送器停止写回 |
| UNCONFIRMED / BACKPRESSURE | 网络、超时、5xx或429后原键有限重报,次数/截止耗尽后转REVIEW |
| AUTHORIZATION / HTTP_REJECTED | 权限或明确HTTP拒绝,转REVIEW;可能已有历史未知受理,不能直接当作未创建 |
| IDEMPOTENCY_CONFLICT | 原键已对应其他内容,停止并核查 |
| INVALID_DOCUMENT / INVALID_RECEIPT / TARGET_MISMATCH | 请求摘要、回执归属或目标不可信,转REVIEW,保留证据 |
| SEND_BUDGET_EXHAUSTED / SEND_DEADLINE | 不再自动发送,保留原请求供人工核查 |
send_count统计本地发送领取,与Pulse的执行尝试次数分别计数;原键重复提交不会新建任务或执行尝试。自动发送期限是恢复安全边界,不代表任务执行期限;任务总时限从平台受理时开始。带绝对available_at/start_deadline_at的命令在积压后可能不再合法,不能为了通过校验修改原键对应的时间字段。迟到的合法成功回执仍可确认,已被新代次替换的发送器只能查询新状态。
## 6. 故障验证与生产边界
`make integration`验证本地并发重复、业务回滚、确认原子性、多发送者领取、旧代次拒绝、背压/权限/预算、精确数字、真实HTTP受理回执丢失、三个阶段真实MySQL COMMIT成功包丢失,以及平台已提交后发送器SIGKILL/新进程自然租约接管。CLI的初始化、enqueue重复、status、send、正常停止和demo也进入回归。详细证据见[验证报告](../reports/phase-3-task-outbox.md)。
本例的业务效果仅为本地SQL记录及平台受理,不提供外部服务恰好一次执行。业务Worker继续承担业务幂等;执行结果需要[可靠结果订阅与Inbox](result-inbox.md)等衔接。敏感请求和回执留在业务数据库,应纳入业务访问控制、审计、备份及保留策略;日志只输出操作ID和稳定状态,不输出token或请求正文。
REVIEW后的人工核查、证据审批及受控恢复由接入方的业务管理流程承担,示例不提供无证据清空错误/重置预算接口。不要直接修改SQL状态制造成功,也不要在无法确认的情况下换业务操作ID。生产MySQL故障切换、长期归档、跨部署迁移和容量仍按P6验收;本例不启动流程或发送Weave信号,相应Outbox命令类型随流程API演进。
相对延迟任务可在SubmitTaskRequest设置DelayMS;Outbox冻结并重发原时长,由Orbit首次受理事务计算执行时间。它不从本地业务创建时间起算;固定业务时刻应使用冻结AvailableAt,详见[延迟接入](delayed-task.md)。