feat: add durable business task outbox example and fault recovery

This commit is contained in:
2026-09-17 23:13:48 +08:00
parent 2b8213a4f7
commit 522fe2ef3e
27 changed files with 2195 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
# Orbit 功能说明:Pulse 与公共能力
> 版本 0.2 · 2026-09-17。Pulse 与 Weave 首版功能范围已确认;标注“建议”的细节待评审。Weave 具体范围另见流程设计。基础、任务执行、调度、资源控制和可靠订阅已实现;批量受理、独立任务核查/人工重试、业务键/创建时间查询及直接来源追踪已实现,事务Outbox接入等继续开发,实现状态见持续开发计划。
> 版本 0.2 · 2026-09-17。Pulse 与 Weave 首版功能范围已确认;标注“建议”的细节待评审。Weave 具体范围另见流程设计。基础、任务执行、调度、资源控制和可靠订阅已实现;批量受理、独立任务核查/人工重试、业务键/创建时间查询及直接来源追踪已实现,业务事务Outbox已提供完整接入示例;整体阶段验收和后续能力见持续开发计划。
## 1. 目标与责任边界

View File

@@ -134,6 +134,8 @@ stateDiagram-v2
Orbit 数据库事务不能自动包含 接入方的业务事务。SDK 的事务接入组件只辅助接入方在自己的数据库写入待发请求,核心 HTTP / WS 客户端不绑定业务 ORM。[事务 Outbox 参考](https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html)
当前[任务Outbox示例](examples/task-outbox.md)已提供独立Go module、业务写入与待发请求同事务、有限租约/退避/预算、原身份核对及CLI。发送前核对命名空间和队列,HTTP在事务外,受理确认与业务任务关联同事务保存;流程启动/信号命令类型随Weave实现。已实现的是业务适配参考,核心SDK不引入MySQL驱动。
### 6.2 平台到结果接收方
结果事件与终态同事务保存,投递状态独立跟踪。接收方可靠处理后确认;确认丢失则重复交付,由接收方去重。

View File

@@ -178,3 +178,7 @@ HTTP/SDK创建时间范围使用包含下界、不包含上界;v2游标以固
### 任务直接来源视图
[来源查询](api/task-source.md)复用Task.OwnerKind/OwnerID/RetryOf、Schedule、ScheduleVersion与ScheduleOccurrence,沿已有(namespace_id,task_id)唯一索引及计划版本键定位,不增加表、持久化字段、索引或迁移。只读响应模型的每个字段也有中文注释。任务、计划、触发版本与队列归属在认证事务中显式验证,缺失关联不被默认为普通任务;流程来源随Weave实现。
### 业务事务Outbox示例模型
[独立task-outbox](examples/task-outbox.md)包含sender_outbox和sender_jobs,2个业务实体、22个Go/SQL中文注释字段,无外键;不计入平台35个实体和289字段。Outbox保存固定命名空间/队列、业务操作、原提交键/请求/摘要、状态/次数/代次/到期时间及原受理回执,业务行同事务创建和关联。命名空间与队列范围内按wake_at索引领取,终结状态wake_at为空;发送次数与总发送期限有界。

View File

@@ -27,7 +27,7 @@
| P0 基础与数据模型 | Go workspace、独立 SDK module、公共/Pulse/Weave 模型、MySQL 迁移、配置、健康检查 | 编译/竞态/静态检查;真实 MySQL 建表、隔离、唯一性、预算、不可变版本、迁移失败防护;SDK 健康调用 | 已完成并通过阶段回归(见 P0 报告) |
| P1 任务受理闭环 | 应用凭据与作用域权限、队列、幂等提交、查询、待执行取消、受理事件;SDK 客户端 | API/SDK 创建查询闭环;重复键与内容冲突、响应未确认重报、跨命名空间拒绝、参数边界 | 已完成并通过阶段回归(见 P1 报告;真实断路/提交 ACK 丢失注入留后续故障阶段) |
| P2 Worker 执行闭环 | WSS 接入、能力/容量、领取与开始、租约、续租、结果确认、排空与恢复;Worker SDK | 真 Worker 执行;失联/重连/重复确认/旧尝试拒绝;业务效果未知转核查;节点故障接管 | 已完成并通过阶段回归:服务端、自动 SDK、历史查询、完整 Schema 与真实进程强杀恢复;测试拓扑和剩余生产故障矩阵见 P2 验收报告 |
| P3 Pulse 完整能力 | 安全重试、延时、Cron/固定间隔、漏跑策略、计划版本、运行中取消、并发许可、配额/背压、可靠结果订阅、批量受理、独立任务核查/人工重试、查询筛选与事务Outbox接入 | 多实例领取/触发竞争、重试上限、取消竞争、队列隔离、积压恢复、事件重复交付 | 进行中:安全重试、计划调度、运行中取消、队列控制/限速/积压背压、跨命名空间应用配额、显式资源键许可及 SDK 场景已实现并通过全量回归;优先级及节点公平派发已实现并通过全量回归;可靠订阅核心、HTTP/WSS、管理 SDK 和管理示例已实现并回归;消费 SDK、协议示例及两项节点强杀回归已完成;业务 Inbox 场景、COMMIT响应丢失及消费者进程故障回归已完成;批量受理、SDK、接口与部分补交示例已完成全量回归;独立任务核查/人工重试、审计查询、两次真实HTTP回执丢失及完整示例已通过全量回归;业务键/创建时间筛选、SDK与示例已通过全量回归;任务直接来源(普通/人工重试/计划固定版本)及SDK示例已通过全量回归;事务Outbox接入待完成 |
| P3 Pulse 完整能力 | 安全重试、延时、Cron/固定间隔、漏跑策略、计划版本、运行中取消、并发许可、配额/背压、可靠结果订阅、批量受理、独立任务核查/人工重试、查询筛选与事务Outbox接入 | 多实例领取/触发竞争、重试上限、取消竞争、队列隔离、积压恢复、事件重复交付 | 进行中:安全重试、计划调度、运行中取消、队列控制/限速/积压背压、跨命名空间应用配额、显式资源键许可及 SDK 场景已实现并通过全量回归;优先级及节点公平派发已实现并通过全量回归;可靠订阅核心、HTTP/WSS、管理 SDK 和管理示例已实现并回归;消费 SDK、协议示例及两项节点强杀回归已完成;业务 Inbox 场景、COMMIT响应丢失及消费者进程故障回归已完成;批量受理、SDK、接口与部分补交示例已完成全量回归;独立任务核查/人工重试、审计查询、两次真实HTTP回执丢失及完整示例已通过全量回归;业务键/创建时间筛选、SDK与示例已通过全量回归;任务直接来源(普通/人工重试/计划固定版本)及SDK示例已通过全量回归;事务Outbox独立业务示例、CLI及故障恢复已实现并通过全量链路回归,待完成P3整体范围验收 |
| P4 Weave 主流程 | SDK 定义构建与发布校验、实例、顺序/分支/结构化并行汇合、信号/定时等待、版本绑定 | 重启接续、早到/重复信号、并行竞争、内部事件重放、发布版本兼容;端到端通用流程 | 待开发 |
| P5 Weave 恢复与查询 | 有界查询、暂停继续、取消、人工核查恢复、控制门控与审计 | 轮次与尝试独立预算;暂停/开始/回执/超时竞争;恢复保留预算;未知结果不盲目重复 | 待开发 |
| P6 生产验证与运维 | 完整监控、凭据轮换、备份恢复、滚动升级、运行手册、可复现负载和故障工具 | 对照生产验收清单执行,报告环境/指标/遗留项;容量、RPO/RTO 按真实拓扑验证 | 待开发 |
@@ -45,13 +45,13 @@ SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最
| 独立任务人工核查与重试 | 已实现[管理API及SDK](api/task-recovery.md)、专门权限、显式安全声明、证据/版本/审计和关联新任务;[完整场景](examples/recovery.md)及[回归报告](reports/phase-3-task-recovery.md)同步交付 |
| 任务查询筛选 | 已实现[业务键及创建时间查询](api/task-queries.md),v2游标绑定全部筛选、旧游标受限兼容、四种索引路径与[完整场景](examples/task-query.md);[回归报告](reports/phase-3-task-queries.md)同步记录 |
| 任务详情来源追踪 | 已实现[独立来源接口](api/task-source.md)、SDK和普通/计划/人工恢复场景,返回计划及触发固定版本;同命名空间归属、当前权限和来源缺失拒绝已通过[全量回归](reports/phase-3-task-source.md),流程来源随Weave接入 |
| 业务到平台的事务Outbox接入 | [事务Outbox设计](04-reliability-and-data.md#61-业务系统到平台) 尚缺完整接入示例;需业务变更与待发请求同事务、稳定提交身份、响应不明核对和发送恢复,保持核心SDK无ORM/数据库依赖 |
| 业务到平台的事务Outbox接入 | 已实现[独立业务模块及完整接入](examples/task-outbox.md),覆盖业务变更/待发请求同事务、有限发送恢复、CLI及真实HTTP/MySQL/进程故障,见[报告](reports/phase-3-task-outbox.md);核心SDK保持无ORM/数据库依赖 |
凭据安全重绑、历史清理、全量监控/容量与MySQL故障切换仍按P6执行;Weave的通用执行门控/内部事件接续随P4–P5落实,不将外部结果订阅当作内部流程引擎。
### P3 事务Outbox接入的剩余验收
### P3 事务Outbox接入的验收范围
以下为下一项实现与测试要求,目前尚未交付:
以下内容已由独立业务示例实现并进入回归,实际证据见[Outbox报告](reports/phase-3-task-outbox.md)。P3仍需逐项对照原功能范围完成整体验收,再推进Weave;不能仅依据本表无待开发行即宣布阶段完成:
1. 独立业务示例module,以业务记录和待发请求同一个MySQL事务提交;核心SDK保持无ORM/数据库依赖,业务表示例也禁止外键,字段提供中文注释。
2. 同时冻结目标命名空间/队列、原幂等键、完整请求及内容摘要。相同业务操作重报不新建待发项,内容不同必须冲突;凭据原文不能进入Outbox或日志。
@@ -77,6 +77,7 @@ SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最
| 优先级与应用隔离 | 近同时到期按优先级 → 旧任务等待补偿 → 应用/队列轮转 | P3,已实现完整示例 |
| 可靠结果订阅 | 冻结结果 → 持久化交付 → Inbox 去重 → 确认/补发 → 耗尽后人工重试 | P3,核心、HTTP/WSS、管理及消费 SDK、独立Inbox示例与真实进程故障回归已完成 |
| 批量文件作业受理 | 逐项身份 → 部分结果 → 同键核对 → 释放额度 → 只补交失败项 | P3,已实现完整示例 |
| 业务事务Outbox | 业务记录/请求同事务 → 原键发送 → 受理/业务关联同事务 → 故障恢复 | P3,已实现独立模块与完整示例 |
| 业务操作定位 | 精确业务键 → 创建区间 → 游标分页 → 查询详情 | P3,已实现完整示例 |
| 独立任务人工恢复 | 未知结果 → 证据核查 → 保留原任务 → 关联新任务 → 审计查询 | P3,已实现完整示例 |
| 并行生成结果 | 发布流程 → 并行执行 → 汇合 → 完成 | P4 |
@@ -114,6 +115,7 @@ SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最
- [P3 人工核查与重试回归](reports/phase-3-task-recovery.md)
- [P3 业务键与时间查询回归](reports/phase-3-task-queries.md)
- [P3 任务直接来源回归](reports/phase-3-task-source.md)
- [P3 业务事务Outbox回归](reports/phase-3-task-outbox.md)
- [接口手册](api/README.md)
- [SDK 接入](sdk/README.md)
- [通用场景示例](examples/README.md)

View File

@@ -106,6 +106,8 @@ curl -i "$ORBIT_URL/v1/tasks" \
SDK:`Client.SubmitTask(ctx, key, request)`。
需要将业务变更与提交可靠衔接时,使用[事务Outbox接入示例](../examples/task-outbox.md):先在业务事务里冻结原操作和请求,再在事务外调用此接口;新增示例不改变本HTTP契约。
## GET /v1/tasks/{task_id}
权限:tasks:read 和任务所在队列范围。200 的 data 直接为上方 task 对象,返回当前状态和完整参数/已保存结果。不存在、跨命名空间或不在该凭据队列范围时返回 404。

View File

@@ -2,6 +2,14 @@
示例只使用通用的作业、对象、文件、资源与审核信号,不包含接入方项目名称或行业规则。
## 已可运行:业务事务与任务提交Outbox
- 独立模块:[task-outbox](../../examples/task-outbox/README.md),[完整前置、命令、Go方法及故障处理](task-outbox.md)。
- 操作:业务记录和待发请求同事务 → 持久化领取 → 原幂等键HTTP提交 → 原回执与业务关联同事务确认。
- 预期:local_duplicate=true、state=ACCEPTED、sends=1;ACCEPTED仅表示平台受理,演示随后取消任务并保留历史。
- 覆盖:8个并发调用/20项领取、业务回滚、旧代次拒绝、HTTP回执丢失、三处MySQL COMMIT回执丢失及发送进程SIGKILL恢复。
- 边界:业务侧两张表均无外键,22个中文注释字段;固定发送预算,REVIEW保留原记录供业务管理核查,核心SDK不引入数据库依赖。
## 已可运行:健康检查
- 源码:[SDK 健康检查](../../sdk/go/examples/health/main.go)。

View File

@@ -0,0 +1,174 @@
# 业务事务与任务提交 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)包含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接入接口
以下方法属于示例的内部业务适配包,完整源码见[store.go](../../examples/task-outbox/internal/outbox/store.go)及[sender.go](../../examples/task-outbox/internal/outbox/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演进。

View File

@@ -0,0 +1,75 @@
# P3 业务事务 Outbox 回归
验证日期:2026-09-17。独立业务示例、发送恢复、CLI和接入文档已实现;定向真实数据库、全量链路及最终输入边界补充回归均已正常退出,状态码0。P3最终范围核对、Weave及生产部署验收继续按计划推进。
## 1. 实现证据
- [独立模块](../../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、当前凭据核对目标、原键重报、背压退避、未知结果分类、过期接管与旧代次拒绝。
- [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)验证:
1. 8个相同业务操作并发入库只有一条业务记录/Outbox,7次重复核对;修改正文、源引用或目标队列不能复用原操作。
2. 实际业务INSERT失败时Outbox一起回滚;业务关联UPDATE失败时ACCEPTED一起回滚,恢复后可保存原回执。
3. 8个领取者处理20条待发项,每项初次只被一个有效代次领取;过期接管沿用提交键,旧代次确认拒绝。
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失败即回滚。
[HTTP回执丢失](../../examples/task-outbox/internal/outbox/http_integration_test.go)先取得真实平台受理结果,再向客户端发送不完整响应并断开;Outbox保持PENDING,原键重报后同一任务ACCEPTED、deduplicated=true、发送次数2,队列只有一项任务。错误持久化命名空间在提交前被拒绝。
[发送进程故障](../../examples/task-outbox/internal/outbox/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及正常信号停止。
## 3. 发现与修正
- 初次故障注入复用了上一笔COMMIT标记,误把后续事务准备响应当作COMMIT成功;错误标记未消费导致代理等待。通过当前进程和数据库状态核实后,对归属本轮的测试进程取得SIGQUIT堆栈,确认问题,再修正逐命令标记及非阻塞故障通知;原测试终态失败后才重新运行。
- 进程恢复的首个断言统计了整个命名空间的业务记录,混入之前HTTP测试的其他队列;改为沿Outbox关联按命名空间和队列核对。平台任务及操作身份没有发生重复。
- HTTP故障先发送成功头与不完整正文,确保客户端看到响应不完整,不把连接层透明重放当成Outbox自身恢复证据。
- 最终复查增加非法UTF-8字符串的入库前拒绝,防止Go JSON编码静默替换业务标识;单元和真实数据库验证均确认业务行与Outbox未写入。此修改后重新完成基础检查和Outbox完整独立链路。
这些修正属于测试注入和验证范围;报告不把早期失败记为通过。
## 4. 环境、复现与全量结果
macOS、Go1.27.1、Docker单实例MySQL8.4.10,临时数据库、随机凭据与回环端口。业务表与平台库分离,但本轮使用同一个隔离MySQL进程,不代表生产跨故障域验收。
```sh
make fmt test vet build
make integration
```
全量脚本和最终Outbox补充脚本均已观察到终态退出码0。没有配置数据库时go test跳过Integration,不能据此声称已覆盖真实故障。
| 验证组 | 本轮实际结果 |
| --- | --- |
| fmt / race / vet / build | PASS;最终UTF-8修改后再次全部通过 |
| 全量MySQL平台组 | PASS,121.429秒;历史外键70→0、323个未替换索引组件保留 |
| SDK Worker | PASS,87.148秒 |
| SDK计划 / 队列 / 批量 / 查询 | PASS,5.607 / 4.957 / 1.407 / 1.362秒 |
| SDK订阅管理 / 消费 | PASS,34.081 / 51.750秒 |
| SDK人工核查与重试 | PASS,3.375秒 |
| Inbox数据库与消费者故障 | PASS,34.989秒 |
| Outbox完整组 | PASS,34.327秒;含CLI 1.34秒、真实发送进程恢复31.16秒 |
| SDK应用额度 | PASS,1.425秒 |
| 九项平台节点/Worker故障 | PASS,62.786秒;加上Inbox与Outbox进程,共十一项进程故障 |
| 完整可运行示例 | 17个核心SDK程序 + Inbox + Outbox,共19个通过 |
| 最终UTF-8增强后的Outbox独立复测 | PASS,33.808秒;真实MySQL、HTTP、CLI、三个COMMIT丢响应与自然租约进程恢复全部重跑,init/demo通过 |
全量链路先通过,随后仅增强独立Outbox模块的非法UTF-8入库校验,并补充完整模块复测;平台、核心SDK和协议代码未改变。文档链接/JSON块、SDK无服务端或MySQL依赖检查通过。耗时是这次正确性回归记录,不是容量或吞吐指标。
## 5. 范围与后续
- 本例证明本地业务事务与单项任务受理的稳定身份衔接,业务实际副作用仍由Worker幂等承担,结果回写使用订阅/Inbox等链路。
- 固定24小时发送窗口、100次领取、30秒租约、原请求重报;不提供无证据重置预算、自动换键或无限恢复。
- REVIEW保留原数据,由接入方带证据和审计的管理流程处置;本示例未实现通用业务审核系统或跨业务ORM适配。
- 核心SDK、平台Schema和执行协议均未改变;新增业务示例不是已发布的兼容性SDK包。
- 流程启动与信号Outbox类型需随Weave API实现;P6继续容量、数据库切换、备份恢复及运维验证。当前正确性与进程恢复证据不等同于生产高可用或百万规模容量结论。

View File

@@ -80,6 +80,8 @@ fmt.Println(current.ID, current.State, current.Revision)
受理回执不是业务执行结果。同键重报返回原始快照;当前状态应使用 GetTask。业务侧创建操作与平台提交之间存在双写边界,接入方应在自身事务中保存操作记录/Outbox,再使用稳定身份提交,不能依靠随机新键补发来“修复”未知结果。
[业务事务Outbox完整场景](../examples/task-outbox.md)已提供独立业务数据库、原子业务写入、持久化发送器、原身份恢复及CLI;核心SDK保持无MySQL/ORM依赖。业务适配包的Enqueue/SendOnce/Status与实际运行命令均在该手册说明。
## 3. 取消与错误处理
```go