Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e56bd633 | |||
| e29dec717f | |||
| 0140c78ea7 | |||
| 6eb429e21f | |||
| 2d8467e50c | |||
| 2f95d9c45c | |||
| c33c12c5e9 | |||
| 81f97544db | |||
| e97b76fe5f | |||
| 6db60bd13b | |||
| d49c739be8 | |||
| 6a25a718c4 | |||
| 6af20c2cd4 | |||
| a6f4aa229d | |||
| 3e1db5e30d | |||
| af997b7c06 | |||
| 45a2b361f2 | |||
| 3c8a9ac47c | |||
| 4cd99b8bb6 | |||
| 875a66b0a4 | |||
| e81af2666d | |||
| c03f8ab3d5 | |||
| 9e3d74443e | |||
| cad4e1206a | |||
| 21a6b61b55 | |||
| 05ebc812df | |||
| bf76f13430 |
22
.env.example
22
.env.example
@@ -1,5 +1,7 @@
|
|||||||
# Example only. Load environment variables explicitly; binaries do not read .env.
|
# Example only. Load environment variables explicitly; binaries do not read .env.
|
||||||
ORBIT_HTTP_ADDR=127.0.0.1:8080
|
ORBIT_HTTP_ADDR=127.0.0.1:8080
|
||||||
|
# 控制台默认队列,不替代登录及账号授权。
|
||||||
|
ORBIT_CONSOLE_QUEUE_ID=
|
||||||
# Set a real DSN through your secret manager. No credentials are committed.
|
# Set a real DSN through your secret manager. No credentials are committed.
|
||||||
ORBIT_MYSQL_DSN=
|
ORBIT_MYSQL_DSN=
|
||||||
ORBIT_DB_MAX_OPEN=20
|
ORBIT_DB_MAX_OPEN=20
|
||||||
@@ -18,6 +20,26 @@ ORBIT_EVENT_CONNECTION_LIMIT=128
|
|||||||
# 每个 WebSocket Hub 的数据库领取并发和有界内存排队,范围分别为1–256、1–65536。
|
# 每个 WebSocket Hub 的数据库领取并发和有界内存排队,范围分别为1–256、1–65536。
|
||||||
ORBIT_DISPATCH_WORKERS=4
|
ORBIT_DISPATCH_WORKERS=4
|
||||||
ORBIT_DISPATCH_CAPACITY=128
|
ORBIT_DISPATCH_CAPACITY=128
|
||||||
|
# 归档策略首次启动时的数据库默认值;保存到管理端后,以数据库版本为准。
|
||||||
|
# Enabled 没有环境变量,默认关闭;窗口仅允许1–3650天,幂等记录不得少于90天。
|
||||||
|
ORBIT_RETENTION_IDEMPOTENCY_DAYS=90
|
||||||
|
ORBIT_RETENTION_TASK_DAYS=365
|
||||||
|
ORBIT_RETENTION_WORKFLOW_DAYS=365
|
||||||
|
ORBIT_RETENTION_EVENT_DAYS=365
|
||||||
|
ORBIT_RETENTION_DELIVERY_DAYS=90
|
||||||
|
ORBIT_RETENTION_INBOX_DAYS=90
|
||||||
|
ORBIT_RETENTION_AUDIT_DAYS=2555
|
||||||
# Empty disables metrics. To enable a separate private listener, use e.g.
|
# Empty disables metrics. To enable a separate private listener, use e.g.
|
||||||
# ORBIT_METRICS_ADDR=127.0.0.1:9090. Never expose it on the public API ingress.
|
# ORBIT_METRICS_ADDR=127.0.0.1:9090. Never expose it on the public API ingress.
|
||||||
ORBIT_METRICS_ADDR=
|
ORBIT_METRICS_ADDR=
|
||||||
|
|
||||||
|
# 仅供 orbit-admin retention archive / verify-archive 使用;每个作业单独的绝对目录。
|
||||||
|
# 不会启动后台定时归档,完整参数见 docs/api/retention-jobs.md。
|
||||||
|
# ORBIT_ARCHIVE_DIRECTORY=/srv/orbit-archives/tasks-20260923
|
||||||
|
|
||||||
|
# 周期归档:持久卷目录和固定卷ID必须一起配置,默认关闭。
|
||||||
|
# 镜像用户65532须有目录权限;共享卷节点必须使用同一物理卷。
|
||||||
|
ORBIT_ARCHIVE_ROOT=
|
||||||
|
ORBIT_ARCHIVE_STORAGE_ID=
|
||||||
|
ORBIT_ARCHIVE_MAX_BYTES=10737418240
|
||||||
|
ORBIT_ARCHIVE_TICK_INTERVAL=10s
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@
|
|||||||
*.log
|
*.log
|
||||||
/.local/
|
/.local/
|
||||||
*.credentials.json
|
*.credentials.json
|
||||||
|
|
||||||
|
/examples/http-service/server
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
- 任务来源通过独立只读路由查询,tasks:read仅授予当前任务的有限来源引用;计划定义仍需schedules:read。显式核对命名空间、队列、计划、触发与固定版本,悬空/错误/未实现所有者不得默认为DIRECT;不依赖外键,也不向派发路径加入来源查询。
|
- 任务来源通过独立只读路由查询,tasks:read仅授予当前任务的有限来源引用;计划定义仍需schedules:read。显式核对命名空间、队列、计划、触发与固定版本,悬空/错误/未实现所有者不得默认为DIRECT;不依赖外键,也不向派发路径加入来源查询。
|
||||||
|
|
||||||
- 业务事务Outbox示例使用独立module和业务数据库,核心SDK不得引入SQL驱动。业务记录/待发命令同事务;原请求、操作ID、目标及提交键冻结,HTTP在事务外,确认按最新租约代次与业务关联同事务。24小时/100次预算内原键核对,禁止换键、重置预算或把ACCEPTED解释成业务成功;REVIEW保留待核查数据。
|
- 业务事务Outbox示例使用独立module和业务数据库,核心SDK不得引入SQL驱动。业务记录/待发命令同事务;原请求、操作ID、目标及提交键冻结,HTTP在事务外,确认按最新租约代次与业务关联同事务。24小时/100次预算内原键核对,禁止换键、重置预算或把ACCEPTED解释成业务成功;REVIEW保留待核查数据。
|
||||||
|
- 可复用业务Outbox位于 adapters/outbox/mysql 独立module。EnqueueTx只写调用方事务,不Commit/Rollback/HTTP;重复核对使用共享锁当前读,避免重复INSERT锁升级死锁。受理钩子与最新代次确认同事务,只允许同库短SQL,固定Outbox→业务行锁序。旧CLI与HTTP宿主共用发送核心;宿主关闭顺序为HTTP收尾→发送/执行收尾→结果消费收尾→数据库关闭,未结清记录保留原身份。
|
||||||
|
|
||||||
- 相对延迟delay_ms只在首次受理事务按数据库时间计算;原时长参与摘要,零值omitempty保持旧摘要。重报先返回原回执,禁止重算执行时间;总时限包含等待。计划模板拒绝非零delay_ms,人工重试不自动继承首次等待。
|
- 相对延迟delay_ms只在首次受理事务按数据库时间计算;原时长参与摘要,零值omitempty保持旧摘要。重报先返回原回执,禁止重算执行时间;总时限包含等待。计划模板拒绝非零delay_ms,人工重试不自动继承首次等待。
|
||||||
|
|
||||||
|
|||||||
35
CHANGELOG.md
Normal file
35
CHANGELOG.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 版本记录
|
||||||
|
|
||||||
|
代码交付主分支为 `main`。采用 `v主版本.次版本.修订号`,已发布标签不可变;补丁按 `v0.1.1`、`v0.1.2` 递增,业务项目固定具体版本。
|
||||||
|
|
||||||
|
## v0.1.1 — 2026-09-24
|
||||||
|
|
||||||
|
本版汇总 v0.1.0 之后已完成的控制台、运维和业务接入能力,并统一示例和接入文档。
|
||||||
|
|
||||||
|
### 新增及完善
|
||||||
|
|
||||||
|
- 控制台账号密码登录、页面访问保护、初始化管理员与登录页;前端自动使用同源服务地址。
|
||||||
|
- Compose 迁移/权限收紧/核验/启动链、域名反向代理示例及两节点部署覆盖配置。
|
||||||
|
- 可配置的历史保留策略、归档文件作业、周期归档计划及控制台作业管理。
|
||||||
|
- 跨节点重连、慢消费者、锁等待/死锁、增长与归档的隔离回归,详细范围见各验收报告。
|
||||||
|
- 独立 MySQL 事务 Outbox 适配包:调用方事务入队、受理确认钩子、持久预算与有界后台发送。
|
||||||
|
- 现有 HTTP 服务嵌入示例:业务事务 → Outbox → Worker → 可靠结果 → Inbox,含健康检查和有界关闭。
|
||||||
|
- 根 `examples/` 集中 27 个可运行场景,各有 README;首页按功能、启动、SDK、配置和运维组织。
|
||||||
|
|
||||||
|
### 版本与升级
|
||||||
|
|
||||||
|
| 发布物 | Git 标签 | 引用方式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 平台源码 | `v0.1.1` | 检出该标签构建服务端 |
|
||||||
|
| Go SDK | `sdk/go/v0.1.1` | `go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.1` |
|
||||||
|
| MySQL Outbox | `adapters/outbox/mysql/v0.1.1` | `go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@v0.1.1` |
|
||||||
|
|
||||||
|
SDK 公共方法与 v0.1.0 保持一致,示例从 `sdk/go/examples/<name>` 移至 `examples/<name>`;原先 `cd sdk/go && go run ./examples/worker` 改为 `cd examples && go run ./worker`。示例不是业务导入包,核心 SDK 导入路径不变。
|
||||||
|
|
||||||
|
服务端数据库基线为 schema v31。已有数据库按[运行手册](docs/20-operations-runbook.md)备份、停旧写节点、执行前向迁移及 `orbit-admin verify`,然后启动新服务;不覆盖旧迁移文件。Outbox 表属于独立业务数据库,由业务迁移管理。
|
||||||
|
|
||||||
|
[本版发布核验](docs/reports/v0.1.1-release.md)记录本地回归、远端版本和外部项目安装结果。保留策略及归档不会自动删除源数据;生产跨故障域高可用和容量仍按[交付清单](docs/release-checklist.md)验收。
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
首个 SDK 接入版本,平台和 SDK 已使用不可变标签发布。远端安装与最初门禁证据见[首版发布报告](docs/reports/phase-7-sdk-remote-release.md)。
|
||||||
29
Makefile
29
Makefile
@@ -1,25 +1,37 @@
|
|||||||
GO ?= go
|
GO ?= go
|
||||||
|
|
||||||
.PHONY: fmt test vet build image integration mysql-replication mysql-compatibility sdk-check protocol-check docs-check schedule-backlog release-manifest release-local-check
|
.PHONY: fmt test vet build adapters-check image integration console-e2e compose-check mysql-replication mysql-compatibility sdk-check protocol-check docs-check stability-check schedule-backlog release-manifest release-local-check
|
||||||
fmt:
|
fmt:
|
||||||
$(GO) fmt ./...
|
$(GO) fmt ./...
|
||||||
cd sdk/go && $(GO) fmt ./...
|
cd sdk/go && $(GO) fmt ./...
|
||||||
|
cd examples && GOWORK=off $(GO) fmt ./...
|
||||||
cd examples/result-inbox && GOWORK=off $(GO) fmt ./...
|
cd examples/result-inbox && GOWORK=off $(GO) fmt ./...
|
||||||
cd examples/task-outbox && GOWORK=off $(GO) fmt ./...
|
cd examples/task-outbox && GOWORK=off $(GO) fmt ./...
|
||||||
|
cd adapters/outbox/mysql && GOWORK=off $(GO) fmt ./...
|
||||||
|
cd examples/http-service && GOWORK=off $(GO) fmt ./...
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GO) test -race ./...
|
$(GO) test -race ./...
|
||||||
cd sdk/go && $(GO) test -race ./...
|
cd sdk/go && $(GO) test -race ./...
|
||||||
|
cd examples && GOWORK=off $(GO) test -race ./...
|
||||||
cd examples/result-inbox && GOWORK=off $(GO) test -race ./...
|
cd examples/result-inbox && GOWORK=off $(GO) test -race ./...
|
||||||
cd examples/task-outbox && GOWORK=off $(GO) test -race ./...
|
cd examples/task-outbox && GOWORK=off $(GO) test -race ./...
|
||||||
|
cd adapters/outbox/mysql && GOWORK=off $(GO) test -race ./...
|
||||||
|
cd examples/http-service && GOWORK=off $(GO) test -race ./...
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
$(GO) vet ./...
|
$(GO) vet ./...
|
||||||
cd sdk/go && $(GO) vet ./...
|
cd sdk/go && $(GO) vet ./...
|
||||||
|
cd examples && GOWORK=off $(GO) vet ./...
|
||||||
cd examples/result-inbox && GOWORK=off $(GO) vet ./...
|
cd examples/result-inbox && GOWORK=off $(GO) vet ./...
|
||||||
cd examples/task-outbox && GOWORK=off $(GO) vet ./...
|
cd examples/task-outbox && GOWORK=off $(GO) vet ./...
|
||||||
|
cd adapters/outbox/mysql && GOWORK=off $(GO) vet ./...
|
||||||
|
cd examples/http-service && GOWORK=off $(GO) vet ./...
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
cd examples && GOWORK=off $(GO) build ./...
|
||||||
|
cd adapters/outbox/mysql && GOWORK=off $(GO) build ./...
|
||||||
|
cd examples/http-service && GOWORK=off $(GO) build -o ../../bin/orbit-http-example ./cmd/server
|
||||||
$(GO) build -o bin/orbit ./cmd/orbit
|
$(GO) build -o bin/orbit ./cmd/orbit
|
||||||
$(GO) build -o bin/orbit-migrate ./cmd/orbit-migrate
|
$(GO) build -o bin/orbit-migrate ./cmd/orbit-migrate
|
||||||
$(GO) build -o bin/orbit-admin ./cmd/orbit-admin
|
$(GO) build -o bin/orbit-admin ./cmd/orbit-admin
|
||||||
@@ -32,6 +44,18 @@ image:
|
|||||||
integration:
|
integration:
|
||||||
./scripts/test-mysql.sh
|
./scripts/test-mysql.sh
|
||||||
|
|
||||||
|
console-e2e:
|
||||||
|
./scripts/test-console-e2e.sh
|
||||||
|
|
||||||
|
stability-check:
|
||||||
|
./scripts/test-stability.sh
|
||||||
|
|
||||||
|
compose-check:
|
||||||
|
@test -n "$${ORBIT_DB_USER:-}" || (echo 'ORBIT_DB_USER is required' >&2; exit 1)
|
||||||
|
@test -n "$${ORBIT_DB_PASSWORD:-}" || (echo 'ORBIT_DB_PASSWORD is required' >&2; exit 1)
|
||||||
|
@test -n "$${ORBIT_DB_ROOT_PASSWORD:-}" || (echo 'ORBIT_DB_ROOT_PASSWORD is required' >&2; exit 1)
|
||||||
|
@docker compose -f deploy/docker-compose.yml config >/dev/null
|
||||||
|
|
||||||
mysql-replication:
|
mysql-replication:
|
||||||
./scripts/test-mysql-replication.sh
|
./scripts/test-mysql-replication.sh
|
||||||
|
|
||||||
@@ -56,3 +80,6 @@ release-local-check:
|
|||||||
# Runs the bounded, intentionally incomplete persistent backlog rehearsal in an isolated MySQL.
|
# Runs the bounded, intentionally incomplete persistent backlog rehearsal in an isolated MySQL.
|
||||||
schedule-backlog:
|
schedule-backlog:
|
||||||
./scripts/test-schedule-backlog.sh
|
./scripts/test-schedule-backlog.sh
|
||||||
|
|
||||||
|
adapters-check:
|
||||||
|
./scripts/test-adapters.sh
|
||||||
|
|||||||
305
README.md
305
README.md
@@ -1,112 +1,277 @@
|
|||||||
# Orbit · 星轨
|
# Orbit · 星轨
|
||||||
|
|
||||||
Orbit 是面向多个业务项目的通用任务调度与流程编排平台,采用 **Go + MySQL + HTTP/WebSocket**。
|
**Go + MySQL 构建的任务调度与流程编排平台。** 业务应用通过 HTTP 提交任务,Worker 通过 WebSocket 执行,结果通过可靠订阅交付;任务状态、计划、流程检查点与审计保存在 MySQL。
|
||||||
|
|
||||||
- **Pulse · 脉动**:异步任务、定时调度、执行授权与安全重试。
|
- **Pulse · 脉动**:异步任务、延迟执行、定时计划、执行授权、重试与结果订阅。
|
||||||
- **Weave · 织流**:研发定义发布流程,管理步骤、等待、有界查询与恢复。
|
- **Weave · 织流**:研发定义并发布流程,支持顺序、并行、条件分支、等待信号、有界查询和人工恢复。
|
||||||
- 业务处理器运行在接入方 Worker 中;平台不内置行业规则。
|
- **Console**:账号密码登录的管理页面,查看任务、计划、流程、结果交付及归档作业。
|
||||||
|
|
||||||
## 当前进度
|
主分支:`main`。当前版本:**`v0.1.1`**。源码、SDK 和 Outbox 适配包采用固定版本发布,已发布标签不覆盖。变更见 [CHANGELOG](CHANGELOG.md)。
|
||||||
|
|
||||||
已完成 P0–P5 平台功能阶段验收:36 个数据模型、版本化迁移、凭据及范围校验、队列与任务 API、WSS 执行、独立 SDK、自动 Worker 和 [执行尝试/证据查询](docs/api/execution-history.md)。[Worker Schema](docs/api/worker-schema.md) 与真实进程强杀恢复回归已通过;[安全重试](docs/api/retries.md)、[定时计划](docs/api/schedules.md)、[运行中取消](docs/api/cancellation.md) 、[队列资源控制](docs/api/queues.md) 、[应用共享配额](docs/api/application-resources.md) 、[资源键互斥](docs/api/keyed-concurrency.md) 和 [优先级/节点公平派发](docs/api/priority-and-fairness.md) 已接入服务端和 SDK,[可靠结果订阅](docs/api/subscriptions.md) 已实现持久化核心、HTTP/WSS、管理 SDK 与示例,[消费 SDK](docs/sdk/event-consumer.md) 和 [独立 MySQL Inbox 示例](docs/examples/result-inbox.md) 已实现;[批量任务受理](docs/api/batch-tasks.md) 已提供逐项幂等、失败分类与 SDK 示例;[人工核查与重试](docs/api/task-recovery.md) 已提供带证据的管理接口、关联新任务、审计查询及运行示例;[业务键与时间范围查询](docs/api/task-queries.md) 已提供过滤、分页与SDK场景;[任务来源查询](docs/api/task-source.md) 已提供人工重试父任务、计划触发记录与固定版本;[业务事务Outbox](docs/examples/task-outbox.md) 已提供独立业务库、稳定提交身份、发送恢复与CLI;[相对延迟执行](docs/examples/delayed-task.md)已提供数据库计时及原请求恢复;完整范围核对见[P3验收](docs/reports/phase-3.md),生产验证与首版交付按 [持续开发计划](docs/08-development-plan.md) 继续推进。
|
## 从这里开始
|
||||||
|
|
||||||
Weave已提供[定义发布](docs/api/workflow-definitions.md)、[实例启动、步骤详情、暂停/继续/取消、信号和操作审计](docs/api/workflows.md),以及[六类人工核查恢复](docs/api/workflow-recovery.md)的HTTP与独立Go SDK。持久运行器支持顺序、分支、有限并行、TIMER、SIGNAL及有界POLL,恢复沿用原预算与截止。[并行作业与回执示例](docs/examples/workflow.md)提供完整调用链;[历史激活/轮次/信号及信号审计](docs/api/workflow-history.md)已接入;[实例筛选分页](docs/api/workflow-queries.md)已接入,[状态事件时间线](docs/api/workflow-events.md)已接入,独立[轮询](docs/examples/workflow-poll.md)与[恢复](docs/examples/workflow-recovery.md)示例已接入,已补[版本/暂停竞争/全停回归](docs/reports/phase-4-5-races.md),功能阶段证据见[P4/P5核对](docs/reports/phase-4-5.md),提交断线及派发未送达见[故障边界](docs/reports/phase-6-fault-boundaries.md);[任务流程来源](docs/api/task-source.md#5-流程固定来源)已接入,P4/P5功能阶段已通过,P6生产验证和P7发布交付继续推进。
|
| 你要做什么 | 入口 |
|
||||||
|
| --- | --- |
|
||||||
|
| 第一次启动平台、设置管理员 | [快速开始](#快速开始) |
|
||||||
|
| 使用 Docker 启动,后续接入域名 | [Docker 与域名部署](#docker-与域名部署) |
|
||||||
|
| 了解全部能力和用法 | [功能一览](#功能一览) |
|
||||||
|
| 业务 Go 项目接入 | [SDK 怎么调用](#sdk-怎么调用) |
|
||||||
|
| 直接运行完整场景 | [examples:27 个可运行示例](examples/README.md) |
|
||||||
|
| 嵌入现有 HTTP 服务 | [HTTP + Outbox + Worker + Inbox](examples/http-service/README.md) |
|
||||||
|
| 查字段、错误码、权限及协议 | [HTTP / WebSocket API](docs/api/README.md)、[Go SDK 手册](docs/sdk/README.md) |
|
||||||
|
| 配置、排障、备份、发布 | [配置与运维](#配置与运维)、[运行手册](docs/20-operations-runbook.md) |
|
||||||
|
|
||||||
数据库禁止外键,关联一致性通过服务端事务校验,索引、唯一约束和 CHECK 保留;新装库使用无外键当前版本基线,已有库沿不可变迁移链升级;readiness 同时检查迁移及零外键要求。[运行观测](docs/api/observability.md)提供固定基数 HTTP、数据库池和后台批次指标;发布前可执行只读的 [`orbit-admin verify`](docs/api/operations.md) 核验迁移、零外键和固定索引。这不表示完整平台已具备生产服务能力。验证证据见 [P0](docs/reports/phase-0.md)、[P1](docs/reports/phase-1.md)、[P2 阶段验收](docs/reports/phase-2.md)、[安全重试](docs/reports/phase-3-safe-retry.md)、[计划调度](docs/reports/phase-3-schedules.md)、[取消与无外键回归](docs/reports/phase-3-cancellation.md) 及 [队列资源回归](docs/reports/phase-3-queue-resources.md)。
|
## 功能一览
|
||||||
|
|
||||||
## 目录
|
每个示例目录都有 README、源码、启动方式和结果核对。接口手册包含字段、权限、错误码和恢复约定。
|
||||||
|
|
||||||
```text
|
### Pulse:任务与调度
|
||||||
cmd/orbit/ 基础 HTTP 服务入口
|
|
||||||
cmd/orbit-migrate/ 独立数据库迁移命令
|
|
||||||
cmd/orbit-admin/ 受控初始化与私密凭据交付
|
|
||||||
model/ 公共 ID、JSON、摘要与记录原语
|
|
||||||
platform/model/ 应用、命名空间、幂等、事件和审计
|
|
||||||
platform/auth/ 凭据格式与动作/队列授权范围
|
|
||||||
pulse/model/ 任务、尝试、队列、计划和执行许可
|
|
||||||
pulse/service/ 任务与队列应用契约、输入校验
|
|
||||||
pulse/worker/ 会话、开始、续租、结果和恢复契约
|
|
||||||
pulse/retry/ 冻结策略、有限退避与未知效果决策
|
|
||||||
pulse/schedule/ Cron/间隔、漏跑、版本及计划接口
|
|
||||||
internal/timezone/ 随计划冻结的内置时区数据快照
|
|
||||||
weave/model/ 定义、实例、步骤、信号和查询轮次
|
|
||||||
weave/definition/ 有限结构/类型校验与定义发布契约
|
|
||||||
weave/engine/ 不可变规则的有限步骤决策与精确结果映射
|
|
||||||
weave/instance/ 实例受理、当前详情及操作审计契约
|
|
||||||
weave/signal/ 内部可靠信号受理/查询契约及显式激活身份
|
|
||||||
weave/control/ 内部版本化暂停/继续/取消契约
|
|
||||||
internal/config/ 配置校验
|
|
||||||
internal/storage/mysql/ 连接、事务、迁移及集成测试
|
|
||||||
internal/transport/httpserver/ HTTP 接入
|
|
||||||
sdk/go/ 独立 Go SDK module 与示例
|
|
||||||
scripts/ 可复现的回归工具
|
|
||||||
docs/ 设计、接口、SDK、场景与验证报告
|
|
||||||
```
|
|
||||||
|
|
||||||
## 本地运行
|
| 功能 | 做什么 / 怎么用 | 可运行示例 / 接口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 即时任务与提交幂等 | `SubmitTask` 提交固定处理器版本与 JSON;重报复用原操作键,`GetTask` 查当前状态 | [tasks](examples/tasks/README.md) · [任务接口](docs/api/tasks.md) |
|
||||||
|
| 延迟任务 | `DelayMS` 从首次受理计时;`AvailableAt` 指定绝对时刻 | [delayed-task](examples/delayed-task/README.md) |
|
||||||
|
| 批量受理 | `SubmitTasks` 最多 32 项,逐项返回受理/拒绝/未知,按原身份补交 | [batch](examples/batch/README.md) · [批量接口](docs/api/batch-tasks.md) |
|
||||||
|
| 自动 Worker | 注册处理器与版本,`NewWorker(...).Run(ctx)` 负责连接、授权、续租、上报与排空 | [worker](examples/worker/README.md) · [运行器手册](docs/sdk/worker-runtime.md) |
|
||||||
|
| 安全重试 | 冻结次数、退避与总时限;明确可重试失败自动再执行,未知效果按安全策略核查 | [retry](examples/retry/README.md) · [重试策略](docs/api/retries.md) |
|
||||||
|
| 取消 | `CancelTask` 保存取消意图;运行中的处理器配合停止并结清 | [cancellation](examples/cancellation/README.md) |
|
||||||
|
| 定时计划 | Cron / 固定间隔、时区、不可变版本、漏跑策略、暂停/恢复和触发历史 | [schedules](examples/schedules/README.md) · [计划接口](docs/api/schedules.md) |
|
||||||
|
| 队列资源控制 | 创建/配置队列并发、开始速率、积压上限和暂停;满额返回背压 | [resources](examples/resources/README.md) · [队列接口](docs/api/queues.md) |
|
||||||
|
| 应用共享配额 | 同一应用的多个命名空间/队列共享总额度 | [application-resources](examples/application-resources/README.md) |
|
||||||
|
| 资源键互斥 | `ConcurrencyKey` 协调跨队列同资源任务,资源端核验递增许可序号 | [keyed-concurrency](examples/keyed-concurrency/README.md) |
|
||||||
|
| 优先级 | 任务优先级 0–9,结合等待时间排序与节点轮转派发 | [priority](examples/priority/README.md) |
|
||||||
|
| 查询与来源 | 按队列、状态、业务键、创建时间分页;追溯普通提交、计划、流程和人工重试来源 | [task-query](examples/task-query/README.md) · [来源接口](docs/api/task-source.md) |
|
||||||
|
| 历史与证据 | 查询每次尝试、原结果及迟到/冲突报告,保留原始记录 | [history](examples/history/README.md) |
|
||||||
|
| 人工核查与重试 | `ReviewTask` 提交证据;`RetryTask` 创建关联新任务并保留审计 | [recovery](examples/recovery/README.md) |
|
||||||
|
|
||||||
要求 Go 1.27.1、MySQL 8.4+。先准备专用数据库和凭据,通过环境变量注入;程序不会自动读取 `.env` 文件。
|
### Weave:流程编排
|
||||||
|
|
||||||
|
| 功能 | 做什么 / 怎么用 | 可运行示例 / 接口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 定义与版本 | 研发使用 SDK 创建定义、发布不可变版本;运行实例固定启动版本 | [workflow-definition](examples/workflow-definition/README.md) · [定义格式](docs/api/workflow-definition-format.md) |
|
||||||
|
| 顺序、条件、并行与汇合 | 用有限结构组合任务与结果映射;持久化每一步和推进检查点 | [workflow](examples/workflow/README.md) · [实例接口](docs/api/workflows.md) |
|
||||||
|
| 时间与外部信号等待 | TIMER 到期继续;SIGNAL 按固定等待身份接收外部回执 | [workflow](examples/workflow/README.md) · [信号设计](docs/14-workflow-signals.md) |
|
||||||
|
| 有界重复查询 | POLL 分别限制业务查询轮次和单轮请求重试,处理中继续等待,耗尽转核查 | [workflow-poll](examples/workflow-poll/README.md) |
|
||||||
|
| 暂停、继续、取消 | 停止推进后续工作,同时收集已发出的在途结果 | [控制接口](docs/api/workflows.md) · [控制语义](docs/15-workflow-controls.md) |
|
||||||
|
| 人工恢复 | 按原因、证据、版本核查任务/步骤/等待;恢复沿用原历史、预算和截止 | [workflow-recovery](examples/workflow-recovery/README.md) · [恢复接口](docs/api/workflow-recovery.md) |
|
||||||
|
| 实例查询与审计 | 筛选分页,查步骤详情、历史激活、查询轮次、信号及状态时间线 | [workflow-query](examples/workflow-query/README.md) · [历史接口](docs/api/workflow-history.md) · [时间线](docs/api/workflow-events.md) |
|
||||||
|
|
||||||
|
### 业务接入与管理
|
||||||
|
|
||||||
|
| 功能 | 做什么 / 怎么用 | 入口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 可靠结果订阅 | 先创建订阅,再由 EventConsumer 通过 WebSocket 接收结果;业务提交后 ACK,失败按预算重新交付 | [subscriptions](examples/subscriptions/README.md) · [消费 SDK](docs/sdk/event-consumer.md) |
|
||||||
|
| 事务 Outbox 公共包 | `EnqueueTx` 将待发请求写入业务事务,`Run` 后台发送;网络结果不明复用原身份 | [MySQL 适配包](docs/sdk/outbox-mysql.md) · [task-outbox](examples/task-outbox/README.md) |
|
||||||
|
| 业务结果 Inbox | 事件去重与业务更新同事务,提交成功后确认交付 | [result-inbox](examples/result-inbox/README.md) |
|
||||||
|
| HTTP 服务完整生命周期 | 业务接口、Outbox、Worker、结果消费、就绪检查和有界关闭放进同一业务服务 | [http-service](examples/http-service/README.md) |
|
||||||
|
| 平台身份与凭据 | 应用/命名空间/队列授权范围、凭据轮换/撤销;SDK 不直连平台数据库 | [鉴权](docs/api/authentication.md) · [凭据管理](docs/api/authentication.md) |
|
||||||
|
| 账号密码控制台 | 初始化管理员,登录查看统计、任务/计划/流程/交付详情和管理操作,未登录跳转登录页 | [Console 手册](docs/console.md) |
|
||||||
|
| 保留策略与周期归档 | 配置各类数据保留天数,持久化归档计划/作业,分页、续跑、重试与文件核验 | [保留策略](docs/api/retention.md) · [周期归档](docs/api/archive-scheduler.md) |
|
||||||
|
| 运维与观测 | 健康/readiness、独立指标端口、迁移核验、负载工具、备份恢复与故障演练 | [观测](docs/api/observability.md) · [运维](docs/20-operations-runbook.md) |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
需要 **Go 1.27.1、MySQL 8.4**,先创建专用数据库 `orbit`;下方管理员密码提示和读取队列 ID 的辅助命令使用 Python 3。程序通过环境变量配置,**不会自动读取 `.env`**。如当前 shell 设置了 `GOTOOLCHAIN=local`,必须安装满足版本要求的 Go。
|
||||||
|
|
||||||
|
### 1. 获取代码并迁移数据库
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
git clone --branch v0.1.1 https://git.yuntuo.hk/k2safe/orbit.git
|
||||||
|
cd orbit
|
||||||
export ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit'
|
export ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit'
|
||||||
export ORBIT_HTTP_ADDR='127.0.0.1:8080'
|
export ORBIT_HTTP_ADDR='127.0.0.1:8080'
|
||||||
go run ./cmd/orbit-migrate up
|
go run ./cmd/orbit-migrate up
|
||||||
|
```
|
||||||
|
|
||||||
|
DSN 替换为实际账号密码。`--branch v0.1.1` 检出固定发布版本;参与开发使用 `git switch main`。已有库先备份,再按[升级与发布手册](docs/20-operations-runbook.md)停旧写节点、迁移和核验;不修改已应用的迁移文件。
|
||||||
|
|
||||||
|
### 2. 一次性创建示例资源与管理员
|
||||||
|
|
||||||
|
下面在独立演示应用中授予所有流程及任务演示所需权限,并设置一个管理员。密码至少 12 字节,通过标准输入传递,不写入命令行或 Git。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
umask 077
|
||||||
|
mkdir -p .local
|
||||||
|
python3 -c 'import getpass; print(getpass.getpass("设置管理员密码(至少12字节):"))' | \
|
||||||
|
go run ./cmd/orbit-admin bootstrap \
|
||||||
|
--application sample-app --namespace default --environment development \
|
||||||
|
--queue examples --worker-access --schedule-access --event-access \
|
||||||
|
--recovery-access --workflow-access --workflow-run-access \
|
||||||
|
--workflow-control-access --workflow-signal-access \
|
||||||
|
--workflow-recovery-access --workflow-audit-access \
|
||||||
|
--console-username admin --console-display-name 'Orbit 管理员' \
|
||||||
|
--console-password-stdin --output .local/development.credentials.json
|
||||||
|
```
|
||||||
|
|
||||||
|
初始化只运行一次,后续重启复用数据库、账号和 SDK 凭据。凭据 JSON 是服务端程序访问 API 的秘密文件,**控制台不需要选择或上传它**。已有资源增加管理员的方法见 [Console 手册](docs/console.md);真实接入按实际角色缩小权限。
|
||||||
|
|
||||||
|
### 3. 启动平台,进入控制台
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export ORBIT_CONSOLE_QUEUE_ID="$(python3 -c 'import json; print(json.load(open(".local/development.credentials.json"))["queue_id"])')"
|
||||||
go run ./cmd/orbit
|
go run ./cmd/orbit
|
||||||
```
|
```
|
||||||
|
|
||||||
上方 DSN 为本地占位示例,需替换为实际凭据。部署时使用秘密管理和经过验证的 TLS 配置;迁移账户与运行账户分开授权。运行程序不自动迁移数据库。
|
打开 **<http://127.0.0.1:8080/console/>**,用 `admin` 和刚设置的密码登录。页面自动使用当前访问地址请求服务端,统计与列表从平台接口读取 MySQL 中的数据。
|
||||||
|
|
||||||
另一个终端执行 SDK 示例:
|
### 4. 跑一条完整执行链
|
||||||
|
|
||||||
|
另开终端,从仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
export ORBIT_URL=http://127.0.0.1:8080
|
||||||
ORBIT_URL=http://127.0.0.1:8080 go run ./examples/health
|
export ORBIT_CREDENTIALS_FILE="$PWD/.local/development.credentials.json"
|
||||||
|
cd examples
|
||||||
|
go run ./health
|
||||||
|
go run ./worker
|
||||||
```
|
```
|
||||||
|
|
||||||
任务接入从 [初始化凭据](docs/api/authentication.md) 开始,再按 [SDK 接入](docs/sdk/README.md) 运行受理/取消示例。[自动 Worker 示例](docs/sdk/worker-runtime.md) 演示处理器注册、并发消费、结果确认与排空;特殊接入也可使用 [底层协议示例](docs/sdk/worker.md)。
|
健康检查显示 `liveness=alive readiness=ready`;Worker 示例实际创建并执行三条文本任务,最后输出:
|
||||||
|
|
||||||
## 开发与回归
|
```text
|
||||||
|
task=<任务ID> state=SUCCEEDED result={"text":"HELLO ORBIT"}
|
||||||
|
task=<任务ID> state=SUCCEEDED result={"text":"REUSABLE WORKER"}
|
||||||
|
task=<任务ID> state=SUCCEEDED result={"text":"DURABLE RESULT"}
|
||||||
|
worker_drained=true started=3 confirmed=3 running=0
|
||||||
|
```
|
||||||
|
|
||||||
|
刷新控制台可查看这些任务。继续在 `examples` 目录运行 `go run ./schedules`、`go run ./workflow` 或 `go run ./workflow-poll` 体验定时和流程;全部 27 个场景及特殊准备条件见 [examples/README.md](examples/README.md)。
|
||||||
|
|
||||||
|
## SDK 怎么调用
|
||||||
|
|
||||||
|
### 安装固定版本
|
||||||
|
|
||||||
|
在自己的业务项目中执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make fmt
|
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.1
|
||||||
make test
|
# 业务事务需要 MySQL Outbox 时再安装:
|
||||||
make vet
|
go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@v0.1.1
|
||||||
make build
|
|
||||||
make image
|
|
||||||
make integration
|
|
||||||
make protocol-check
|
|
||||||
make docs-check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
首版发布前按[交付核对清单](docs/release-checklist.md)逐项留存命令输出和目标环境证据;当前阶段回归不替代生产拓扑、容量、复制切换或 RPO/RTO 验收。
|
无需本地源码或 `replace`。SDK 是独立 Go module,只依赖标准库和 WebSocket 客户端;MySQL 驱动属于可选适配包。子模块的 Git 标签分别为 `sdk/go/v0.1.1` 和 `adapters/outbox/mysql/v0.1.1`。
|
||||||
|
|
||||||
`make integration` 要求 Docker、OpenSSL 等标准命令,启动独立的临时 MySQL 容器,使用随机凭据和回环随机端口;完成后删除该测试容器。不会连接已有业务数据库。可通过 `GO`、`DOCKER` 和 `ORBIT_TEST_MYSQL_IMAGE` 指定工具/测试镜像。
|
### 提交、执行、接收结果
|
||||||
|
|
||||||
受理层负载采样工具见 [有界受理负载工具](docs/api/load.md),只允许在隔离命名空间或一次性数据库中运行;它不自动重试、不启动 Worker,也不提供生产容量承诺。
|
一次完整接入分为三个角色,可以分进程,也可以嵌入同一业务 HTTP 服务:
|
||||||
|
|
||||||
真实Worker与任务完成路径测量使用[`orbit-exec-load`](docs/api/execution-load.md):短/混合/中长模拟任务、持久结果和单次尝试核验、阶段延迟分布;不修改队列额度、不调用外部业务系统。定时积压复现可运行 `make schedule-backlog`,它只使用一次性 MySQL,并保留有界截止负例的实际分母。Weave 并行流程链路使用[`orbit-workflow-load`](docs/api/workflow-load.md),核对固定版本、步骤、来源和最终输出。定时集中触发使用[`orbit-schedule-load`](docs/api/schedule-load.md),逐项核对触发、任务、唯一尝试与来源,并保留超时分母和暂停收尾证据。
|
```text
|
||||||
|
业务事务 → Outbox → HTTP 提交到 Orbit → WebSocket Worker 执行业务
|
||||||
|
↓
|
||||||
|
业务数据 ← Inbox ← WebSocket 结果订阅 ← 持久化结果
|
||||||
|
```
|
||||||
|
|
||||||
SDK 独立发布门禁使用 `make sdk-check`,在 `GOWORK=off` 下检查完整依赖图,并运行 SDK 的竞态测试、vet 和全包构建;规则和接入边界见 [SDK 发布门禁](docs/sdk/release.md)。
|
下面是业务函数中的提交片段;完整 imports、main、Worker 注册和收尾见 [worker/main.go](examples/worker/main.go)。`baseURL` 为实际平台地址,例如 `https://orbit.example.com`;`token` 和 `queueID` 从业务配置/Secret 注入。
|
||||||
|
|
||||||
修改已应用迁移时会触发校验和拒绝;新增结构使用新的前向迁移。初始结构尚未部署生产。
|
```go
|
||||||
|
client, err := orbit.NewClient(baseURL, orbit.Options{Token: token})
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
## 文档入口
|
// operationKey 和完整 request 必须先保存在业务库;网络重报仍使用原值。
|
||||||
|
request := orbit.SubmitTaskRequest{
|
||||||
|
QueueID: queueID,
|
||||||
|
Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1",
|
||||||
|
Payload: json.RawMessage(`{"object_ref":"objects/input"}`),
|
||||||
|
TaskTimeoutMS: 60000,
|
||||||
|
Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 10000},
|
||||||
|
RecoverySafety: orbit.RecoveryReconcileOnly,
|
||||||
|
}
|
||||||
|
accepted, err := client.SubmitTask(ctx, operationKey, request)
|
||||||
|
if err != nil { return err }
|
||||||
|
current, err := client.GetTask(ctx, accepted.Task.ID)
|
||||||
|
if err != nil { return err }
|
||||||
|
fmt.Println(current.ID, current.State)
|
||||||
|
```
|
||||||
|
|
||||||
- [持续开发与回归计划](docs/08-development-plan.md)
|
导入路径为 `orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"`。处理器的名称、处理器版本和载荷版本必须与 Worker 注册能力一致;没有匹配的在线 Worker 时,提交成功也不会立即执行。
|
||||||
- [数据模型与约束](docs/07-data-model.md)
|
|
||||||
- [HTTP / WebSocket 接口手册](docs/api/README.md)
|
|
||||||
- [Go SDK 接入](docs/sdk/README.md)
|
|
||||||
- [通用场景示例](docs/examples/README.md)
|
|
||||||
- [运行与发布手册](docs/20-operations-runbook.md)
|
|
||||||
- [容器发布说明](docs/21-container-deployment.md)
|
|
||||||
- [设计文档总览](docs/README.md)
|
|
||||||
|
|
||||||
Git 远端:[k2safe/orbit](https://git.yuntuo.hk/k2safe/orbit),主分支 `main`。首个接入版本为 `v0.1.0`,业务项目使用 Go 1.27.1 或更高版本安装:
|
**任务受理不等于执行成功。** 同键重报返回原回执,最新状态用 `GetTask` 或结果事件核对。平台不能替外部系统保证“恰好执行一次”;副作用须使用业务幂等键、事务或证据核查。
|
||||||
|
|
||||||
|
结果通知使用 `NewEventConsumer(...).Run(ctx)`。先创建持久订阅,再提交希望接收的任务;消费函数在 Inbox 与业务变更明确提交后返回 nil,SDK 才 ACK。断线重连、重复交付和业务去重见[消费手册](docs/sdk/event-consumer.md)。
|
||||||
|
|
||||||
|
### 现有 HTTP 服务如何接入
|
||||||
|
|
||||||
|
直接从 [examples/http-service](examples/http-service/README.md) 开始,完整命令、配置及 curl 请求见[运行手册](docs/examples/http-service.md)。该工程提供:
|
||||||
|
|
||||||
|
1. `init`:在独立业务库创建 Outbox、业务表、Inbox 和结果视图。
|
||||||
|
2. `provision`:创建可靠结果订阅,将订阅 ID 保存到部署配置。
|
||||||
|
3. `serve`:启动业务 HTTP、Outbox 发送器、Worker 和结果消费者。
|
||||||
|
4. 业务请求返回本地受理状态;随后查询到平台 TaskID、执行结果和业务视图。
|
||||||
|
5. 收到退出信号后,依次收尾 HTTP、发送器/Worker、结果消费者,最后关闭数据库。
|
||||||
|
|
||||||
|
只抽取发送器时使用[公共 Outbox 包](adapters/outbox/mysql/README.md):`EnqueueTx` 加入调用方事务,不提交事务、不发网络请求;`Run` 在后台提交并核对平台回执。业务确认钩子与 Outbox 确认同事务执行。
|
||||||
|
|
||||||
|
## Docker 与域名部署
|
||||||
|
|
||||||
|
仓库提供 [Dockerfile](Dockerfile)、[Compose 启动链](deploy/docker-compose.yml)和 [Nginx 配置](deploy/nginx/orbit.conf.example)。按[完整 Compose 手册](docs/22-compose-deployment.md)注入数据库密码、构建镜像、启动 MySQL/迁移/核验、初始化管理员,最后启动 Orbit。手册也包括停止、更新与两节点覆盖配置。
|
||||||
|
|
||||||
|
- Console 同源访问:域名反向代理到 Orbit 后,页面自动使用这个域名,无需在界面填写服务地址。
|
||||||
|
- 业务 SDK:把自己的 `baseURL` / 示例 `ORBIT_URL` 配置为该 HTTPS 域名,SDK 使用相应 WSS 地址。
|
||||||
|
- 网关保留 WebSocket Upgrade/Connection 头,健康检查使用 `/health/ready`。
|
||||||
|
- 停止容器保留 MySQL 和归档持久卷;更换镜像不等于删除数据。
|
||||||
|
|
||||||
|
## 配置与运维
|
||||||
|
|
||||||
|
配置完整示例见 [.env.example](.env.example)。由 shell、部署平台或 Secret 管理器显式注入:
|
||||||
|
|
||||||
|
| 配置 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ORBIT_MYSQL_DSN` | 平台数据库连接,迁移账号与运行账号按部署手册分开 |
|
||||||
|
| `ORBIT_HTTP_ADDR` | 服务进程监听地址,例 `127.0.0.1:8080` |
|
||||||
|
| `ORBIT_CONSOLE_QUEUE_ID` | 控制台默认队列;不授予账号额外权限 |
|
||||||
|
| `ORBIT_DB_MAX_OPEN` / `ORBIT_DB_MAX_IDLE` | 数据库连接池上限 |
|
||||||
|
| `ORBIT_SHUTDOWN_TIMEOUT` | SIGTERM 关闭窗口,默认 10s |
|
||||||
|
| `ORBIT_WORKFLOW_TICK_INTERVAL` / `ORBIT_SCHEDULE_TICK_INTERVAL` | 流程/计划后台扫描间隔,默认 1s |
|
||||||
|
| `ORBIT_RECOVERY_TICK_INTERVAL` / `ORBIT_RUNTIME_BATCH_LIMIT` | 恢复扫描间隔与每批上限,默认 2s / 100 |
|
||||||
|
| `ORBIT_WORKER_CONNECTION_LIMIT` / `ORBIT_EVENT_CONNECTION_LIMIT` | 每节点 WS 连接上限,默认各 128 |
|
||||||
|
| `ORBIT_DISPATCH_WORKERS` / `ORBIT_DISPATCH_CAPACITY` | 派发并发和有界内存队列,默认 4 / 128 |
|
||||||
|
| `ORBIT_METRICS_ADDR` | 独立内部指标监听,默认关闭 |
|
||||||
|
| `ORBIT_RETENTION_*_DAYS` | 首次初始化保留窗口;控制台保存后以数据库策略为准 |
|
||||||
|
| `ORBIT_ARCHIVE_ROOT` / `ORBIT_ARCHIVE_STORAGE_ID` | 周期归档卷路径与固定卷身份,二者一起配置 |
|
||||||
|
| `ORBIT_ARCHIVE_MAX_BYTES` / `ORBIT_ARCHIVE_TICK_INTERVAL` | 归档卷预算和调度间隔,默认 10 GiB / 10s |
|
||||||
|
|
||||||
|
默认保留窗口:任务、流程、事件各 **365 天**,交付、Inbox、幂等记录各 **90 天**,审计 **2555 天**。控制台可修改策略并配置周期归档;未结清任务、未知效果、未确认交付等受保护记录不会按时间简单处理。**当前归档导出并保留源数据,尚未提供源数据自动删除执行器。** 详细规则见[保留策略](docs/api/retention.md)和[归档作业](docs/api/archive-scheduler.md)。
|
||||||
|
|
||||||
|
常用检查:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.0
|
curl --fail http://127.0.0.1:8080/health/ready
|
||||||
|
go run ./cmd/orbit-admin verify
|
||||||
|
go run ./cmd/orbit-admin observe snapshot
|
||||||
```
|
```
|
||||||
|
|
||||||
无需本地 `replace`;导入方式、完整调用与版本边界见 [SDK 接入](docs/sdk/README.md#5-外部项目通过远端接入)。
|
数据库禁止外键和级联删除;关联一致性由事务校验,保留唯一约束、索引和 CHECK。持久化模型及 SQL 字段都有中文注释。已有迁移不可回改,演进采用前向迁移。
|
||||||
|
|
||||||
[流程实例筛选分页](docs/api/workflow-queries.md)已提供独立SDK和[完整示例](docs/examples/workflow-query.md),按固定版本全部队列鉴权,数据库保持零外键。
|
## 项目目录
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/ 平台、数据库迁移和管理命令
|
||||||
|
platform/ 应用、身份、授权、幂等、事件、审计
|
||||||
|
pulse/ 任务、Worker、重试、队列、定时计划
|
||||||
|
weave/ 流程定义、实例、步骤、等待和恢复
|
||||||
|
model/ 公共持久化类型与校验
|
||||||
|
internal/ MySQL 存储、HTTP/WS、配置与观测
|
||||||
|
sdk/go/ 可独立引用的 Go SDK
|
||||||
|
adapters/outbox/mysql/ 可独立引用的 MySQL Outbox 包
|
||||||
|
examples/ 27 个可运行场景及各自 README
|
||||||
|
deploy/ Compose、Nginx 和部署配置
|
||||||
|
scripts/ 构建、回归、故障和发布核验
|
||||||
|
docs/ API、SDK、场景、设计及验收报告
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试、版本与边界
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make test vet build # 7 个模块的竞态测试、静态检查与构建
|
||||||
|
make sdk-check protocol-check docs-check
|
||||||
|
make integration # 临时 MySQL、真实平台、示例及故障回归
|
||||||
|
make console-e2e # 控制台端到端回归
|
||||||
|
```
|
||||||
|
|
||||||
|
集成测试需要 Docker、Python 3 和 OpenSSL 等工具,由脚本创建独立临时数据库并清理自建容器。生产容量与故障域验收另见[交付清单](docs/release-checklist.md);有限样本不能代表百万企业规模的生产容量。
|
||||||
|
|
||||||
|
`v0.1.1` 包含账号密码控制台、归档策略/周期作业、Outbox 公共包、HTTP 生命周期示例及统一示例目录;与上一版本的升级说明和验证结果见 [CHANGELOG](CHANGELOG.md) 与 [发布记录](docs/reports/v0.1.1-release.md)。当前未提供源数据删除、其他数据库 Outbox 适配,也不把单机或隔离环境多节点测试等同于生产跨故障域高可用验收。
|
||||||
|
|
||||||
|
更多资料:[设计文档](docs/README.md) · [持续开发计划](docs/08-development-plan.md) · [接口目录](docs/api/README.md) · [示例目录](examples/README.md) · [发布规则](docs/sdk/release.md)。
|
||||||
|
|||||||
22
adapters/outbox/mysql/README.md
Normal file
22
adapters/outbox/mysql/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Orbit MySQL 事务 Outbox 适配包
|
||||||
|
|
||||||
|
独立 Go module:`git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql`,包名 `outbox`。依赖公开 SDK v0.1.0 与 MySQL 驱动,不依赖平台服务端代码;核心 SDK 不反向依赖本包。
|
||||||
|
|
||||||
|
- [接口、事务边界、错误与恢复](../../../docs/sdk/outbox-mysql.md)
|
||||||
|
- [完整 HTTP 服务接入](../../../docs/examples/http-service.md)
|
||||||
|
- [中文模型](model.go)、[无外键表结构](schema.sql)
|
||||||
|
- [测试报告](../../../docs/reports/outbox-http-integration.md)
|
||||||
|
|
||||||
|
以业务自己的数据库存储待发命令,调用 `EnqueueTx` 加入已有事务,后台运行 `Run` 或 `SendOnce`。本包确认的是平台受理,任务执行完成通过 Worker 和可靠结果订阅衔接。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@v0.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
import outbox "git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql"
|
||||||
|
```
|
||||||
|
|
||||||
|
Git 标签为 `adapters/outbox/mysql/v0.1.1`,无需本地 replace。先将 `SchemaSQL()` 纳入自己的业务迁移,再构造 `Store`;完整事务和运行器代码见上方接口手册。
|
||||||
60
adapters/outbox/mysql/business_fixture_test.go
Normal file
60
adapters/outbox/mysql/business_fixture_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package outbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func businessStore(db *sql.DB, namespace, queue string) (*Store, error) {
|
||||||
|
return New(db, namespace, queue, Options{OnAccepted: accepted})
|
||||||
|
}
|
||||||
|
func accepted(ctx context.Context, tx *sql.Tx, r Record, receipt orbit.SubmitTaskResult) error {
|
||||||
|
var existing *string
|
||||||
|
if err := tx.QueryRowContext(ctx, `SELECT task_id FROM sender_jobs WHERE namespace_id=? AND operation_id=? FOR UPDATE`, r.NamespaceID, r.OperationID).Scan(&existing); err != nil {
|
||||||
|
return ErrStorage
|
||||||
|
}
|
||||||
|
if existing != nil && *existing != receipt.Task.ID {
|
||||||
|
return ErrConflict
|
||||||
|
}
|
||||||
|
_, err := tx.ExecContext(ctx, `UPDATE sender_jobs SET task_id=?,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND operation_id=?`, receipt.Task.ID, r.NamespaceID, r.OperationID)
|
||||||
|
if err != nil {
|
||||||
|
return ErrStorage
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *Store) Enqueue(ctx context.Context, id, source string, in orbit.SubmitTaskRequest) (bool, error) {
|
||||||
|
if !utf8.ValidString(source) || strings.TrimSpace(source) == "" || utf8.RuneCountInString(source) > 512 || strings.ContainsFunc(source, unicode.IsControl) {
|
||||||
|
return false, ErrInvalid
|
||||||
|
}
|
||||||
|
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||||
|
if err != nil {
|
||||||
|
return false, ErrStorage
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
duplicate, err := s.EnqueueTx(ctx, tx, id, in)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if duplicate {
|
||||||
|
var saved string
|
||||||
|
if err = tx.QueryRowContext(ctx, `SELECT source_ref FROM sender_jobs WHERE namespace_id=? AND operation_id=? FOR UPDATE`, s.namespace, id).Scan(&saved); err != nil {
|
||||||
|
return false, ErrStorage
|
||||||
|
}
|
||||||
|
if saved != source {
|
||||||
|
return false, ErrConflict
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err = tx.ExecContext(ctx, `INSERT INTO sender_jobs(namespace_id,operation_id,source_ref,created_at,updated_at) VALUES(?,?,?,UTC_TIMESTAMP(6),UTC_TIMESTAMP(6))`, s.namespace, id, source); err != nil {
|
||||||
|
return false, ErrStorage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tx.Commit() != nil {
|
||||||
|
return false, ErrCommitUnconfirmed
|
||||||
|
}
|
||||||
|
return duplicate, nil
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ func TestIntegrationSenderCLI(t *testing.T) {
|
|||||||
ctx, stop := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, stop := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer stop()
|
defer stop()
|
||||||
binary := filepath.Join(t.TempDir(), "sender")
|
binary := filepath.Join(t.TempDir(), "sender")
|
||||||
build := exec.CommandContext(ctx, filepath.Join(runtime.GOROOT(), "bin", "go"), "build", "-o", binary, "../../cmd/sender")
|
build := exec.CommandContext(ctx, filepath.Join(runtime.GOROOT(), "bin", "go"), "build", "-o", binary, "./cmd/sender")
|
||||||
|
build.Dir = "../../../examples/task-outbox"
|
||||||
if raw, err := build.CombinedOutput(); err != nil {
|
if raw, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("build sender: %v %s", err, raw)
|
t.Fatalf("build sender: %v %s", err, raw)
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ func TestIntegrationSenderCommitResponseLoss(t *testing.T) {
|
|||||||
for _, phase := range []string{"enqueue", "claim", "confirm"} {
|
for _, phase := range []string{"enqueue", "claim", "confirm"} {
|
||||||
t.Run(phase, func(t *testing.T) {
|
t.Run(phase, func(t *testing.T) {
|
||||||
s := testStore(t, proxied)
|
s := testStore(t, proxied)
|
||||||
fresh, _ := New(direct, s.namespace, s.queue)
|
fresh, _ := businessStore(direct, s.namespace, s.queue)
|
||||||
id := testID(t)
|
id := testID(t)
|
||||||
in := testRequest(s.queue)
|
in := testRequest(s.queue)
|
||||||
if phase == "enqueue" {
|
if phase == "enqueue" {
|
||||||
13
adapters/outbox/mysql/go.mod
Normal file
13
adapters/outbox/mysql/go.mod
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql
|
||||||
|
|
||||||
|
go 1.27.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.yuntuo.hk/k2safe/orbit/sdk/go v0.1.0
|
||||||
|
github.com/go-sql-driver/mysql v1.10.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
github.com/coder/websocket v1.8.15 // indirect
|
||||||
|
)
|
||||||
8
adapters/outbox/mysql/go.sum
Normal file
8
adapters/outbox/mysql/go.sum
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
git.yuntuo.hk/k2safe/orbit/sdk/go v0.1.0 h1:OJp+U8dDNKjiXy4ZhO4MMq68Ek8TkjphUAqsjOOg77c=
|
||||||
|
git.yuntuo.hk/k2safe/orbit/sdk/go v0.1.0/go.mod h1:9KlMW7Gy+EGaDoEwr3wHHFY47mnAymSfYeQpMS7h4ag=
|
||||||
|
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||||
|
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
@@ -53,7 +53,7 @@ func realStore(t *testing.T) (*Store, *orbit.Client, string, string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
s, err := New(db, queue.Queue.NamespaceID, queue.Queue.ID)
|
s, err := businessStore(db, queue.Queue.NamespaceID, queue.Queue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ func TestIntegrationSenderHTTPReceiptLoss(t *testing.T) {
|
|||||||
t.Fatal("remote duplicate task", len(page.Items), err)
|
t.Fatal("remote duplicate task", len(page.Items), err)
|
||||||
}
|
}
|
||||||
// 相同凭据搭配错误的持久化目标命名空间,必须在提交前停止。
|
// 相同凭据搭配错误的持久化目标命名空间,必须在提交前停止。
|
||||||
wrong, _ := New(s.db, testID(t), s.queue)
|
wrong, _ := businessStore(s.db, testID(t), s.queue)
|
||||||
wrongID := enqueue(t, wrong)
|
wrongID := enqueue(t, wrong)
|
||||||
r, err = wrong.SendOnce(ctx, through)
|
r, err = wrong.SendOnce(ctx, through)
|
||||||
if err != nil || r.OperationID != wrongID || r.State != "REVIEW" || r.LastCode != "TARGET_MISMATCH" || posts.Load() != 2 {
|
if err != nil || r.OperationID != wrongID || r.State != "REVIEW" || r.LastCode != "TARGET_MISMATCH" || posts.Load() != 2 {
|
||||||
23
adapters/outbox/mysql/model.go
Normal file
23
adapters/outbox/mysql/model.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package outbox
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Record 是业务数据库中的待发命令;只确认平台受理,不能表示业务执行成功。
|
||||||
|
type Record struct {
|
||||||
|
NamespaceID string // NamespaceID 固定目标命名空间,换凭据不能改变归属。
|
||||||
|
QueueID string // QueueID 固定目标队列,发送前通过当前凭据核对归属。
|
||||||
|
OperationID string // OperationID 调用方持久化的业务操作ID,同命名空间内唯一。
|
||||||
|
IdempotencyKey string // IdempotencyKey 由操作ID派生的固定提交键,重报禁止换键。
|
||||||
|
RequestJSON []byte // RequestJSON 冻结的任务提交正文,保留数字精度,不含凭据。
|
||||||
|
RequestDigest []byte // RequestDigest 冻结正文的SHA256,发送前重新校验,非平台语义摘要。
|
||||||
|
State string // State PENDING待发、SENDING领取中、ACCEPTED已受理、REVIEW需核查。
|
||||||
|
SendCount uint32 // SendCount 持久化领取次数,包含领取后未发HTTP即退出的次数,上限100。
|
||||||
|
LeaseEpoch uint64 // LeaseEpoch 每次领取递增;旧代次不能更新新代次的本地结果。
|
||||||
|
WakeAt *time.Time // WakeAt PENDING的下次领取时间或SENDING的租约期限;终结状态为空,UTC微秒。
|
||||||
|
SendDeadlineAt time.Time // SendDeadlineAt 首次入库后24小时停止新发送,短于平台90天幂等窗口。
|
||||||
|
LastCode string // LastCode 最近一次稳定分类,不保存HTTP错误正文、凭据或业务秘密。
|
||||||
|
TaskID *string // TaskID 仅ACCEPTED记录平台任务ID,其余状态为空。
|
||||||
|
ReceiptJSON []byte // ReceiptJSON 仅ACCEPTED保存原受理回执;不是当前执行状态。
|
||||||
|
CreatedAt time.Time // CreatedAt 业务事务首次提交命令的UTC时间,微秒精度。
|
||||||
|
UpdatedAt time.Time // UpdatedAt 最近状态变化的UTC时间,不能替代租约代次。
|
||||||
|
}
|
||||||
19
adapters/outbox/mysql/options.go
Normal file
19
adapters/outbox/mysql/options.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package outbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AcceptedHook 在最新租约确认事务内关联业务数据;可能在事务回滚/提交不明后再次调用。
|
||||||
|
// 必须同库、幂等、遵守 context,只做短 SQL;不能 Commit/Rollback,也不能发网络请求。
|
||||||
|
// record 是锁定后的发送记录,receipt 是本次已核验的受理回执,不是任务完成结果。
|
||||||
|
type AcceptedHook func(context.Context, *sql.Tx, Record, orbit.SubmitTaskResult) error
|
||||||
|
|
||||||
|
// Options 在构造后固定,Store 可以在多个发送 goroutine 中复用。
|
||||||
|
type Options struct {
|
||||||
|
// OnAccepted 可选的业务关联;为空时受理关系仅保存在 Outbox 的 TaskID/ReceiptJSON。
|
||||||
|
OnAccepted AcceptedHook
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ func TestSenderProcessHelper(t *testing.T) {
|
|||||||
}
|
}
|
||||||
db, _ := integrationDB(t)
|
db, _ := integrationDB(t)
|
||||||
_, base, token := realClient(t)
|
_, base, token := realClient(t)
|
||||||
s, err := New(db, os.Getenv("ORBIT_TEST_SENDER_NAMESPACE"), os.Getenv("ORBIT_TEST_SENDER_QUEUE"))
|
s, err := businessStore(db, os.Getenv("ORBIT_TEST_SENDER_NAMESPACE"), os.Getenv("ORBIT_TEST_SENDER_QUEUE"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
76
adapters/outbox/mysql/runtime.go
Normal file
76
adapters/outbox/mysql/runtime.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package outbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrShutdownTimeout = errors.New("outbox sender shutdown deadline exceeded; reconcile persisted records")
|
||||||
|
|
||||||
|
// RunOptions 控制进程轮询与关闭,不改变每条记录持久化的 24 小时/100 次预算。
|
||||||
|
type RunOptions struct {
|
||||||
|
PollInterval time.Duration // PollInterval 空闲/退避轮询间隔,默认500毫秒,范围10毫秒至10秒。
|
||||||
|
ShutdownTimeout time.Duration // ShutdownTimeout 取消后等待当前发送及本地确认,默认20秒,最多5分钟。
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 持续发送;取消后停止领取,限时完成当前交换及确认,调用方必须等待返回。
|
||||||
|
// 存储/确认错误返回给宿主处理;HTTP暂时失败持久化退避,REVIEW留在数据库供核查。
|
||||||
|
// 可以多进程并行运行;数据库租约协调领取,同一记录的HTTP仍可能重复,始终复用原键。
|
||||||
|
func (s *Store) Run(parent context.Context, api *orbit.Client, options RunOptions) error {
|
||||||
|
if api == nil {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
if options.PollInterval == 0 {
|
||||||
|
options.PollInterval = 500 * time.Millisecond
|
||||||
|
}
|
||||||
|
if options.ShutdownTimeout == 0 {
|
||||||
|
options.ShutdownTimeout = 20 * time.Second
|
||||||
|
}
|
||||||
|
if options.PollInterval < 10*time.Millisecond || options.PollInterval > 10*time.Second || options.ShutdownTimeout < time.Millisecond || options.ShutdownTimeout > 5*time.Minute {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
work, cancel := context.WithCancel(context.WithoutCancel(parent))
|
||||||
|
done := make(chan struct{})
|
||||||
|
expired := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
select {
|
||||||
|
case <-work.Done():
|
||||||
|
return
|
||||||
|
case <-parent.Done():
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(options.ShutdownTimeout)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-work.Done():
|
||||||
|
case <-timer.C:
|
||||||
|
close(expired)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() { cancel(); <-done }()
|
||||||
|
for parent.Err() == nil {
|
||||||
|
_, err := s.SendOnce(work, api)
|
||||||
|
select {
|
||||||
|
case <-expired:
|
||||||
|
return ErrShutdownTimeout
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, ErrIdle) && !errors.Is(err, ErrLeaseLost) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(options.PollInterval)
|
||||||
|
select {
|
||||||
|
case <-parent.Done():
|
||||||
|
timer.Stop()
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
32
adapters/outbox/mysql/schema.sql
Normal file
32
adapters/outbox/mysql/schema.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- 业务数据库的初始Outbox表,保留原示例表名以兼容,不属于平台迁移,无外键或级联删除。
|
||||||
|
CREATE TABLE sender_outbox (
|
||||||
|
namespace_id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '固定目标命名空间,重启及换凭据不得改变',
|
||||||
|
queue_id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '固定目标队列,发送前核对当前授权及命名空间',
|
||||||
|
operation_id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '业务操作标识,同命名空间内唯一,调用方持久化后复用',
|
||||||
|
idempotency_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '冻结平台提交键,由业务操作标识派生,禁止换键补发',
|
||||||
|
request_json MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '冻结任务提交正文,保留精确数字,不存认证凭据',
|
||||||
|
request_digest BINARY(32) NOT NULL COMMENT '冻结正文SHA256,防止同身份不同内容,非平台摘要算法',
|
||||||
|
state VARCHAR(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT 'PENDING待发,SENDING领取中,ACCEPTED已受理,REVIEW需核查',
|
||||||
|
send_count INT UNSIGNED NOT NULL COMMENT '领取次数,包含领取后崩溃的次数,最多100',
|
||||||
|
lease_epoch BIGINT UNSIGNED NOT NULL COMMENT '领取代次递增,旧发送器不能覆盖新代次结果',
|
||||||
|
wake_at DATETIME(6) NULL COMMENT '待发下次时间或发送租约期限,终结后为空,UTC微秒',
|
||||||
|
send_deadline_at DATETIME(6) NOT NULL COMMENT '首次入库24小时后的发送截止,短于平台幂等保留窗口',
|
||||||
|
last_code VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '最近稳定分类,不保存服务端错误正文或秘密',
|
||||||
|
task_id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NULL COMMENT '已确认的平台任务标识,只在ACCEPTED非空',
|
||||||
|
receipt_json MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '原受理回执,仅ACCEPTED非空,不代表当前执行结果',
|
||||||
|
created_at DATETIME(6) NOT NULL COMMENT '业务事务首次创建命令UTC时间,微秒精度',
|
||||||
|
updated_at DATETIME(6) NOT NULL COMMENT '本地状态最近变化UTC时间,微秒精度',
|
||||||
|
PRIMARY KEY (namespace_id,operation_id),
|
||||||
|
UNIQUE KEY uq_sender_identity(namespace_id,idempotency_key),
|
||||||
|
KEY ix_sender_due(namespace_id,queue_id,wake_at,operation_id),
|
||||||
|
CONSTRAINT ck_sender_document CHECK (JSON_VALID(request_json)),
|
||||||
|
CONSTRAINT ck_sender_receipt CHECK (receipt_json IS NULL OR JSON_VALID(receipt_json)),
|
||||||
|
CONSTRAINT ck_sender_count CHECK (send_count<=100 AND lease_epoch=send_count),
|
||||||
|
CONSTRAINT ck_sender_state CHECK (
|
||||||
|
(state IN ('PENDING','SENDING') AND wake_at IS NOT NULL AND task_id IS NULL AND receipt_json IS NULL)
|
||||||
|
OR (state='REVIEW' AND wake_at IS NULL AND task_id IS NULL AND receipt_json IS NULL)
|
||||||
|
OR (state='ACCEPTED' AND wake_at IS NULL AND task_id IS NOT NULL AND receipt_json IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT ck_sender_lease CHECK (state<>'SENDING' OR lease_epoch>0),
|
||||||
|
CONSTRAINT ck_sender_deadline CHECK (send_deadline_at>created_at AND send_deadline_at<=TIMESTAMPADD(HOUR,24,created_at))
|
||||||
|
) ENGINE=InnoDB COMMENT='业务事务Outbox,持久化原请求与有限重发状态,不承担业务恰好一次';
|
||||||
311
adapters/outbox/mysql/store.go
Normal file
311
adapters/outbox/mysql/store.go
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
// Package outbox 将待发任务加入调用方的 MySQL 事务,并通过有限租约发送器恢复提交。
|
||||||
|
package outbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed schema.sql
|
||||||
|
var schema string
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalid = errors.New("outbox scope, operation or request is invalid")
|
||||||
|
ErrConflict = errors.New("outbox operation already has different content; keep the original identity")
|
||||||
|
ErrStorage = errors.New("outbox storage failed; reconcile the original operation")
|
||||||
|
ErrCommitUnconfirmed = errors.New("outbox commit is unconfirmed; reconcile the original operation")
|
||||||
|
ErrIdle = errors.New("no due outbox record")
|
||||||
|
ErrNotFound = errors.New("outbox operation not found in this scope")
|
||||||
|
ErrAcceptance = errors.New("outbox acceptance hook failed; confirmation rolled back")
|
||||||
|
ErrLeaseLost = errors.New("outbox lease was replaced; do not overwrite its result")
|
||||||
|
idPattern = regexp.MustCompile(`^[a-f0-9]{32}$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxSends = 100
|
||||||
|
const recordColumns = `namespace_id,queue_id,operation_id,idempotency_key,request_json,request_digest,state,send_count,lease_epoch,wake_at,send_deadline_at,last_code,task_id,receipt_json,created_at,updated_at`
|
||||||
|
|
||||||
|
// Store 固定一个业务数据库及目标范围,不拥有连接池的关闭权。
|
||||||
|
type Store struct {
|
||||||
|
db *sql.DB
|
||||||
|
namespace, queue string
|
||||||
|
onAccepted AcceptedHook
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open 仅连接业务方数据库,不创建表,不输出原始DSN或数据库错误。
|
||||||
|
func Open(ctx context.Context, dsn string) (*sql.DB, error) {
|
||||||
|
cfg, err := mysql.ParseDSN(dsn)
|
||||||
|
if err != nil || cfg.DBName == "" {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
cfg.ParseTime = true
|
||||||
|
cfg.Loc = time.UTC
|
||||||
|
cfg.MultiStatements = false
|
||||||
|
cfg.AllowAllFiles = false
|
||||||
|
cfg.Timeout, cfg.ReadTimeout, cfg.WriteTimeout = 5*time.Second, 8*time.Second, 8*time.Second
|
||||||
|
if cfg.Params == nil {
|
||||||
|
cfg.Params = map[string]string{}
|
||||||
|
}
|
||||||
|
cfg.Params["time_zone"] = "'+00:00'"
|
||||||
|
connector, err := mysql.NewConnector(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrStorage
|
||||||
|
}
|
||||||
|
db := sql.OpenDB(connector)
|
||||||
|
db.SetMaxOpenConns(8)
|
||||||
|
db.SetMaxIdleConns(4)
|
||||||
|
db.SetConnMaxLifetime(5 * time.Minute)
|
||||||
|
if err = db.PingContext(ctx); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, ErrStorage
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaSQL 返回首版无外键表结构,供业务迁移工具管理;不会自动执行。
|
||||||
|
func SchemaSQL() string { return schema }
|
||||||
|
|
||||||
|
// Init 显式创建适配表,已有表会报错;生产升级由业务迁移工具管理。
|
||||||
|
func Init(ctx context.Context, db *sql.DB) error {
|
||||||
|
for _, statement := range strings.Split(schema, ";") {
|
||||||
|
if strings.TrimSpace(statement) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, statement); err != nil {
|
||||||
|
return ErrStorage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 构造可并发复用的适配器,不执行建表或网络请求。
|
||||||
|
func New(db *sql.DB, namespace, queue string, options Options) (*Store, error) {
|
||||||
|
if db == nil || !idPattern.MatchString(namespace) || !idPattern.MatchString(queue) {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
return &Store{db: db, namespace: namespace, queue: queue, onAccepted: options.OnAccepted}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// document 冻结当前v1默认值及UTC表示;Outbox按文档字节去重,不冒充平台语义摘要。
|
||||||
|
func (s *Store) document(in orbit.SubmitTaskRequest) ([]byte, error) {
|
||||||
|
// encoding/json会把非法UTF-8替换成U+FFFD;在冻结摘要前拒绝,不能静默改变业务标识。
|
||||||
|
for _, value := range []string{in.QueueID, in.Handler, in.HandlerVersion, in.PayloadVersion, in.BusinessKey, in.ConcurrencyKey} {
|
||||||
|
if !utf8.ValidString(value) {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.QueueID != s.queue || in.Handler == "" || in.HandlerVersion == "" || in.PayloadVersion == "" || len(in.Payload) > 256<<10 || !utf8.Valid(in.Payload) || !json.Valid(in.Payload) || bytes.Equal(bytes.TrimSpace(in.Payload), []byte("null")) {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
if in.TaskTimeoutMS == 0 {
|
||||||
|
in.TaskTimeoutMS = 86400000
|
||||||
|
}
|
||||||
|
if in.RecoverySafety == "" {
|
||||||
|
in.RecoverySafety = orbit.RecoveryReconcileOnly
|
||||||
|
}
|
||||||
|
if in.Retry.MaxAttempts == 0 {
|
||||||
|
in.Retry.MaxAttempts = 1
|
||||||
|
}
|
||||||
|
if in.Retry.AttemptTimeoutMS == 0 {
|
||||||
|
in.Retry.AttemptTimeoutMS = 30000
|
||||||
|
}
|
||||||
|
if in.Retry.MaxAttempts > 1 {
|
||||||
|
if in.Retry.InitialDelayMS == 0 {
|
||||||
|
in.Retry.InitialDelayMS = 1000
|
||||||
|
}
|
||||||
|
if in.Retry.MaxDelayMS == 0 {
|
||||||
|
in.Retry.MaxDelayMS = 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.AvailableAt != nil {
|
||||||
|
at := in.AvailableAt.UTC()
|
||||||
|
in.AvailableAt = &at
|
||||||
|
}
|
||||||
|
if in.StartDeadlineAt != nil {
|
||||||
|
at := in.StartDeadlineAt.UTC()
|
||||||
|
in.StartDeadlineAt = &at
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(in)
|
||||||
|
if err != nil || len(raw) > 512<<10 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnqueueTx 只写入调用方事务,不开启/提交/回滚事务,不进行 HTTP 请求。
|
||||||
|
// tx 必须来自 New 使用的同一业务数据库。失败时调用方必须回滚整个事务。
|
||||||
|
// duplicate 只表示事务内已有同身份同内容记录,调用方仍必须确认 Commit 成功。
|
||||||
|
func (s *Store) EnqueueTx(ctx context.Context, tx *sql.Tx, operationID string, in orbit.SubmitTaskRequest) (duplicate bool, err error) {
|
||||||
|
if tx == nil || !idPattern.MatchString(operationID) {
|
||||||
|
return false, ErrInvalid
|
||||||
|
}
|
||||||
|
raw, err := s.document(in)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(raw)
|
||||||
|
_, err = tx.ExecContext(ctx, `INSERT INTO sender_outbox(namespace_id,queue_id,operation_id,idempotency_key,request_json,request_digest,state,send_count,lease_epoch,wake_at,send_deadline_at,last_code,created_at,updated_at) VALUES(?,?,?,?,?,?,'PENDING',0,0,UTC_TIMESTAMP(6),TIMESTAMPADD(HOUR,24,UTC_TIMESTAMP(6)),'',UTC_TIMESTAMP(6),UTC_TIMESTAMP(6))`, s.namespace, s.queue, operationID, "outbox-task-"+operationID, raw, digest[:])
|
||||||
|
if err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
var duplicateKey *mysql.MySQLError
|
||||||
|
if !errors.As(err, &duplicateKey) || duplicateKey.Number != 1062 {
|
||||||
|
return false, ErrStorage
|
||||||
|
}
|
||||||
|
var saved []byte
|
||||||
|
var queue, key string
|
||||||
|
// 共享锁当前读:核对 RR 旧快照外的竞争提交,且不把重复 INSERT 的共享锁升级为排它锁。
|
||||||
|
err = tx.QueryRowContext(ctx, `SELECT request_digest,queue_id,idempotency_key FROM sender_outbox WHERE namespace_id=? AND operation_id=? FOR SHARE`, s.namespace, operationID).Scan(&saved, &queue, &key)
|
||||||
|
if err != nil {
|
||||||
|
return false, ErrStorage
|
||||||
|
}
|
||||||
|
if queue != s.queue || key != "outbox-task-"+operationID || !bytes.Equal(saved, digest[:]) {
|
||||||
|
return false, ErrConflict
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type scanner interface{ Scan(...any) error }
|
||||||
|
|
||||||
|
func scanRecord(row scanner, r *Record, extra ...any) error {
|
||||||
|
args := []any{&r.NamespaceID, &r.QueueID, &r.OperationID, &r.IdempotencyKey, &r.RequestJSON, &r.RequestDigest, &r.State, &r.SendCount, &r.LeaseEpoch, &r.WakeAt, &r.SendDeadlineAt, &r.LastCode, &r.TaskID, &r.ReceiptJSON, &r.CreatedAt, &r.UpdatedAt}
|
||||||
|
return row.Scan(append(args, extra...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status 读取固定命名空间和队列内的本地发送记录,不查询平台执行结果。
|
||||||
|
func (s *Store) Status(parent context.Context, id string) (Record, error) {
|
||||||
|
if !idPattern.MatchString(id) {
|
||||||
|
return Record{}, ErrInvalid
|
||||||
|
}
|
||||||
|
ctx, stop := context.WithTimeout(parent, 5*time.Second)
|
||||||
|
defer stop()
|
||||||
|
var r Record
|
||||||
|
err := scanRecord(s.db.QueryRowContext(ctx, `SELECT `+recordColumns+` FROM sender_outbox WHERE namespace_id=? AND queue_id=? AND operation_id=?`, s.namespace, s.queue, id), &r)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return Record{}, ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, ErrStorage
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) claim(parent context.Context) (Record, error) {
|
||||||
|
ctx, stop := context.WithTimeout(parent, 5*time.Second)
|
||||||
|
defer stop()
|
||||||
|
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, ErrStorage
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
var r Record
|
||||||
|
var now time.Time
|
||||||
|
err = scanRecord(tx.QueryRowContext(ctx, `SELECT `+recordColumns+`,UTC_TIMESTAMP(6) FROM sender_outbox FORCE INDEX(ix_sender_due) WHERE namespace_id=? AND queue_id=? AND wake_at<=UTC_TIMESTAMP(6) ORDER BY wake_at,operation_id LIMIT 1 FOR UPDATE SKIP LOCKED`, s.namespace, s.queue), &r, &now)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return Record{}, ErrIdle
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, ErrStorage
|
||||||
|
}
|
||||||
|
if r.State != "PENDING" && r.State != "SENDING" {
|
||||||
|
return Record{}, ErrStorage
|
||||||
|
}
|
||||||
|
if !now.Before(r.SendDeadlineAt) || r.SendCount >= maxSends {
|
||||||
|
r.State = "REVIEW"
|
||||||
|
r.WakeAt = nil
|
||||||
|
r.LastCode = "SEND_BUDGET_EXHAUSTED"
|
||||||
|
if !now.Before(r.SendDeadlineAt) {
|
||||||
|
r.LastCode = "SEND_DEADLINE"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.State = "SENDING"
|
||||||
|
r.SendCount++
|
||||||
|
r.LeaseEpoch++
|
||||||
|
wake := now.Add(30 * time.Second)
|
||||||
|
if wake.After(r.SendDeadlineAt) {
|
||||||
|
wake = r.SendDeadlineAt
|
||||||
|
}
|
||||||
|
r.WakeAt = &wake
|
||||||
|
}
|
||||||
|
_, err = tx.ExecContext(ctx, `UPDATE sender_outbox SET state=?,send_count=?,lease_epoch=?,wake_at=?,last_code=?,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND operation_id=?`, r.State, r.SendCount, r.LeaseEpoch, r.WakeAt, r.LastCode, s.namespace, r.OperationID)
|
||||||
|
if err != nil {
|
||||||
|
return Record{}, ErrStorage
|
||||||
|
}
|
||||||
|
if tx.Commit() != nil {
|
||||||
|
return Record{}, ErrCommitUnconfirmed
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish 只接受最近代次;过期但尚未被替代的成功回执可落库,重复HTTP只复用原键。
|
||||||
|
func (s *Store) finish(parent context.Context, claim Record, state, code string, delay time.Duration, receipt *orbit.SubmitTaskResult) error {
|
||||||
|
ctx, stop := context.WithTimeout(parent, 5*time.Second)
|
||||||
|
defer stop()
|
||||||
|
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||||
|
if err != nil {
|
||||||
|
return ErrStorage
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
var current Record
|
||||||
|
var now time.Time
|
||||||
|
err = scanRecord(tx.QueryRowContext(ctx, `SELECT `+recordColumns+`,UTC_TIMESTAMP(6) FROM sender_outbox WHERE namespace_id=? AND queue_id=? AND operation_id=? FOR UPDATE`, s.namespace, s.queue, claim.OperationID), ¤t, &now)
|
||||||
|
if err != nil {
|
||||||
|
return ErrStorage
|
||||||
|
}
|
||||||
|
if current.State != "SENDING" || current.LeaseEpoch != claim.LeaseEpoch {
|
||||||
|
return ErrLeaseLost
|
||||||
|
}
|
||||||
|
var wake *time.Time
|
||||||
|
var task *string
|
||||||
|
var raw []byte
|
||||||
|
if state == "ACCEPTED" {
|
||||||
|
if receipt == nil || !validReceipt(current, *receipt) {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
raw, err = json.Marshal(receipt)
|
||||||
|
if err != nil || len(raw) > 1<<20 {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
task = &receipt.Task.ID
|
||||||
|
// 持锁顺序:Outbox → 业务行。回调只能做短 SQL,不得提交、回滚或执行网络副作用。
|
||||||
|
if s.onAccepted != nil {
|
||||||
|
if err = s.onAccepted(ctx, tx, current, *receipt); err != nil {
|
||||||
|
return ErrAcceptance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if state == "PENDING" {
|
||||||
|
if delay < time.Second || delay > time.Hour {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
if !now.Before(current.SendDeadlineAt) || current.SendCount >= maxSends {
|
||||||
|
state = "REVIEW"
|
||||||
|
code = "SEND_BUDGET_EXHAUSTED"
|
||||||
|
} else {
|
||||||
|
next := now.Add(delay)
|
||||||
|
if next.After(current.SendDeadlineAt) {
|
||||||
|
next = current.SendDeadlineAt
|
||||||
|
}
|
||||||
|
wake = &next
|
||||||
|
}
|
||||||
|
} else if state != "REVIEW" {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
if _, err = tx.ExecContext(ctx, `UPDATE sender_outbox SET state=?,last_code=?,wake_at=?,task_id=?,receipt_json=?,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND operation_id=?`, state, code, wake, task, raw, s.namespace, current.OperationID); err != nil {
|
||||||
|
return ErrStorage
|
||||||
|
}
|
||||||
|
if tx.Commit() != nil {
|
||||||
|
return ErrCommitUnconfirmed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ func integrationDB(t *testing.T) (*sql.DB, string) {
|
|||||||
}
|
}
|
||||||
func testStore(t *testing.T, db *sql.DB) *Store {
|
func testStore(t *testing.T, db *sql.DB) *Store {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
s, err := New(db, testID(t), testID(t))
|
s, err := businessStore(db, testID(t), testID(t))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ func TestIntegrationSenderConcurrentEnqueueAndConflict(t *testing.T) {
|
|||||||
if _, err := s.Enqueue(context.Background(), id, "objects/test", in); !errors.Is(err, ErrConflict) {
|
if _, err := s.Enqueue(context.Background(), id, "objects/test", in); !errors.Is(err, ErrConflict) {
|
||||||
t.Fatal("request content conflict lost", err)
|
t.Fatal("request content conflict lost", err)
|
||||||
}
|
}
|
||||||
other, _ := New(db, s.namespace, testID(t))
|
other, _ := businessStore(db, s.namespace, testID(t))
|
||||||
if _, err := other.Status(context.Background(), id); !errors.Is(err, ErrNotFound) {
|
if _, err := other.Status(context.Background(), id); !errors.Is(err, ErrNotFound) {
|
||||||
t.Fatal("cross queue record returned", err)
|
t.Fatal("cross queue record returned", err)
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ func TestIntegrationSenderBusinessRollbackAndConfirmationAtomicity(t *testing.T)
|
|||||||
}
|
}
|
||||||
receipt := fakeReceipt(t, s)
|
receipt := fakeReceipt(t, s)
|
||||||
execSQL(t, db, `CREATE TRIGGER sender_update_fail BEFORE UPDATE ON sender_jobs FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='injected business confirmation failure'`)
|
execSQL(t, db, `CREATE TRIGGER sender_update_fail BEFORE UPDATE ON sender_jobs FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='injected business confirmation failure'`)
|
||||||
if err = s.finish(context.Background(), claim, "ACCEPTED", "ACCEPTED", 0, &receipt); !errors.Is(err, ErrStorage) {
|
if err = s.finish(context.Background(), claim, "ACCEPTED", "ACCEPTED", 0, &receipt); !errors.Is(err, ErrAcceptance) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if r := status(t, s, id); r.State != "SENDING" || r.TaskID != nil {
|
if r := status(t, s, id); r.State != "SENDING" || r.TaskID != nil {
|
||||||
179
adapters/outbox/mysql/transaction_integration_test.go
Normal file
179
adapters/outbox/mysql/transaction_integration_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package outbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntegrationCallerOwnedTransaction(t *testing.T) {
|
||||||
|
db, _ := integrationDB(t)
|
||||||
|
s, err := New(db, testID(t), testID(t), Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
id := testID(t)
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
dup, err := s.EnqueueTx(ctx, tx, id, testRequest(s.queue))
|
||||||
|
if err != nil || dup {
|
||||||
|
t.Fatal(dup, err)
|
||||||
|
}
|
||||||
|
if dup, err = s.EnqueueTx(ctx, tx, id, testRequest(s.queue)); err != nil || !dup {
|
||||||
|
t.Fatal("same transaction duplicate not recognized", dup, err)
|
||||||
|
}
|
||||||
|
// 从另一连接看不到未提交的待发任务;包不得偷偷提交,也不得发起HTTP。
|
||||||
|
if _, err = s.Status(ctx, id); !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Fatal("adapter committed caller transaction", err)
|
||||||
|
}
|
||||||
|
if _, err = s.claim(ctx); !errors.Is(err, ErrIdle) {
|
||||||
|
t.Fatal("uncommitted task was claimable", err)
|
||||||
|
}
|
||||||
|
if err = tx.Rollback(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = s.Status(ctx, id); !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Fatal("caller rollback lost", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先在RR事务建立旧快照,再由另一事务提交相同身份,重复核对必须是当前读。
|
||||||
|
tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
var n int
|
||||||
|
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM sender_outbox WHERE namespace_id=?`, s.namespace).Scan(&n); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
other, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer other.Rollback()
|
||||||
|
if _, err = s.EnqueueTx(ctx, other, id, testRequest(s.queue)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = other.Commit(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if dup, err = s.EnqueueTx(ctx, tx, id, testRequest(s.queue)); err != nil || !dup {
|
||||||
|
t.Fatal("RR current read failed", dup, err)
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
claim, err := s.claim(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
receipt := fakeReceipt(t, s)
|
||||||
|
if err = s.finish(ctx, claim, "ACCEPTED", "ACCEPTED", 0, &receipt); err != nil {
|
||||||
|
t.Fatal("optional hook requires business schema", err)
|
||||||
|
}
|
||||||
|
if status(t, s, id).State != "ACCEPTED" {
|
||||||
|
t.Fatal("missing acceptance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationSenderRuntimeDrain(t *testing.T) {
|
||||||
|
for _, timeout := range []bool{false, true} {
|
||||||
|
t.Run(map[bool]string{false: "finish_inflight", true: "bounded_timeout"}[timeout], func(t *testing.T) {
|
||||||
|
db, _ := integrationDB(t)
|
||||||
|
s, err := New(db, testID(t), testID(t), Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
id := testID(t)
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
if _, err = s.EnqueueTx(context.Background(), tx, id, testRequest(s.queue)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
entered := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
unblock := sync.OnceFunc(func() { close(release) })
|
||||||
|
receipt := fakeReceipt(t, s)
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.Method == "GET" {
|
||||||
|
writeTestJSON(w, map[string]any{"request_id": id, "data": orbit.Queue{ID: s.queue, NamespaceID: s.namespace}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(entered)
|
||||||
|
select {
|
||||||
|
case <-release:
|
||||||
|
case <-r.Context().Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(201)
|
||||||
|
writeTestJSON(w, map[string]any{"request_id": id, "data": receipt})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
defer unblock()
|
||||||
|
api, err := orbit.NewClient(server.URL, orbit.Options{Token: "test"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
grace := time.Second
|
||||||
|
if timeout {
|
||||||
|
grace = 20 * time.Millisecond
|
||||||
|
}
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- s.Run(ctx, api, RunOptions{ShutdownTimeout: grace}) }()
|
||||||
|
select {
|
||||||
|
case <-entered:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("sender did not enter HTTP")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
if !timeout {
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatal("did not drain", err)
|
||||||
|
case <-time.After(25 * time.Millisecond):
|
||||||
|
}
|
||||||
|
unblock()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("shutdown hung")
|
||||||
|
}
|
||||||
|
if timeout {
|
||||||
|
if !errors.Is(err, ErrShutdownTimeout) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if status(t, s, id).State == "ACCEPTED" {
|
||||||
|
t.Fatal("timeout fabricated acceptance")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil || status(t, s, id).State != "ACCEPTED" {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTestJSON(w http.ResponseWriter, v any) { _ = json.NewEncoder(w).Encode(v) }
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
|
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
|
||||||
@@ -26,13 +27,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
func run() error {
|
func run() error {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
return errors.New("usage: orbit-admin release manifest|bootstrap|console-user create|verify|observe snapshot|retention inventory|archive|verify-archive|credential status|rotate|revoke|apply")
|
||||||
}
|
}
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "release":
|
case "release":
|
||||||
return runRelease(os.Args[2:])
|
return runRelease(os.Args[2:])
|
||||||
case "bootstrap":
|
case "bootstrap":
|
||||||
return runBootstrap(os.Args[2:])
|
return runBootstrap(os.Args[2:])
|
||||||
|
case "console-user":
|
||||||
|
return runConsoleUser(os.Args[2:])
|
||||||
case "verify":
|
case "verify":
|
||||||
return runVerify(os.Args[2:])
|
return runVerify(os.Args[2:])
|
||||||
case "observe":
|
case "observe":
|
||||||
@@ -42,7 +45,7 @@ func run() error {
|
|||||||
case "credential":
|
case "credential":
|
||||||
return runCredential(os.Args[2:])
|
return runCredential(os.Args[2:])
|
||||||
}
|
}
|
||||||
return errors.New("usage: orbit-admin release manifest|bootstrap|verify|observe snapshot|retention inventory|credential status|rotate|revoke|apply")
|
return errors.New("usage: orbit-admin release manifest|bootstrap|console-user create|verify|observe snapshot|retention inventory|archive|verify-archive|credential status|rotate|revoke|apply")
|
||||||
}
|
}
|
||||||
|
|
||||||
// releaseManifest 是发布物自身的静态身份清单,不读取数据库、不包含 DSN、凭据或业务数据。
|
// releaseManifest 是发布物自身的静态身份清单,不读取数据库、不包含 DSN、凭据或业务数据。
|
||||||
@@ -205,8 +208,16 @@ func runObserve(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runRetention(args []string) error {
|
func runRetention(args []string) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
switch args[0] {
|
||||||
|
case "archive":
|
||||||
|
return runRetentionArchive(args[1:])
|
||||||
|
case "verify-archive":
|
||||||
|
return runRetentionVerifyArchive(args[1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(args) == 0 || args[0] != "inventory" {
|
if len(args) == 0 || args[0] != "inventory" {
|
||||||
return errors.New("usage: orbit-admin retention inventory --before RFC3339")
|
return errors.New("usage: orbit-admin retention inventory --before RFC3339 | archive | verify-archive")
|
||||||
}
|
}
|
||||||
flags := flag.NewFlagSet("retention inventory", flag.ContinueOnError)
|
flags := flag.NewFlagSet("retention inventory", flag.ContinueOnError)
|
||||||
var before string
|
var before string
|
||||||
@@ -255,6 +266,11 @@ func runBootstrap(args []string) error {
|
|||||||
flags.StringVar(&in.Environment, "environment", "", "environment name")
|
flags.StringVar(&in.Environment, "environment", "", "environment name")
|
||||||
flags.StringVar(&in.Queue, "queue", "", "initial queue name")
|
flags.StringVar(&in.Queue, "queue", "", "initial queue name")
|
||||||
flags.StringVar(&output, "output", "", "new private credentials JSON file")
|
flags.StringVar(&output, "output", "", "new private credentials JSON file")
|
||||||
|
var consoleUsername, consoleDisplayName, consolePassword string
|
||||||
|
var consolePasswordStdin bool
|
||||||
|
flags.StringVar(&consoleUsername, "console-username", "", "optional first console administrator username")
|
||||||
|
flags.StringVar(&consoleDisplayName, "console-display-name", "", "optional console administrator display name")
|
||||||
|
flags.BoolVar(&consolePasswordStdin, "console-password-stdin", false, "read the first console administrator password from stdin")
|
||||||
flags.BoolVar(&in.WorkerAccess, "worker-access", false, "also grant Worker connect and consume actions for development")
|
flags.BoolVar(&in.WorkerAccess, "worker-access", false, "also grant Worker connect and consume actions for development")
|
||||||
flags.BoolVar(&in.ScheduleAccess, "schedule-access", false, "also grant schedule read and manage actions for development")
|
flags.BoolVar(&in.ScheduleAccess, "schedule-access", false, "also grant schedule read and manage actions for development")
|
||||||
flags.BoolVar(&in.EventAccess, "event-access", false, "also grant result subscription read, manage and consume actions")
|
flags.BoolVar(&in.EventAccess, "event-access", false, "also grant result subscription read, manage and consume actions")
|
||||||
@@ -271,6 +287,20 @@ func runBootstrap(args []string) error {
|
|||||||
if flags.NArg() != 0 || output == "" {
|
if flags.NArg() != 0 || output == "" {
|
||||||
return errors.New("--output and named resource arguments are required")
|
return errors.New("--output and named resource arguments are required")
|
||||||
}
|
}
|
||||||
|
if consolePasswordStdin {
|
||||||
|
raw, err := io.ReadAll(io.LimitReader(os.Stdin, 1025))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("console administrator password could not be read")
|
||||||
|
}
|
||||||
|
consolePassword = strings.TrimRight(string(raw), "\r\n")
|
||||||
|
}
|
||||||
|
if consoleUsername == "" && (consolePasswordStdin || consoleDisplayName != "") {
|
||||||
|
return errors.New("--console-username is required with console administrator options")
|
||||||
|
}
|
||||||
|
if consoleUsername != "" && !consolePasswordStdin {
|
||||||
|
return errors.New("--console-password-stdin is required when creating the first console administrator")
|
||||||
|
}
|
||||||
|
in.ConsoleUsername, in.ConsoleDisplayName, in.ConsolePassword = consoleUsername, consoleDisplayName, consolePassword
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -324,3 +354,78 @@ func runBootstrap(args []string) error {
|
|||||||
fmt.Println("Bootstrap committed. Credentials saved in the requested private file; valid for 30 days.")
|
fmt.Println("Bootstrap committed. Credentials saved in the requested private file; valid for 30 days.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runConsoleUser 创建一个控制台登录账号。密码默认从标准输入读取,避免出现在
|
||||||
|
// shell 历史和进程列表;账号只绑定已有业务凭据,不会生成或输出业务 token。
|
||||||
|
func runConsoleUser(args []string) error {
|
||||||
|
if len(args) == 0 || args[0] != "create" {
|
||||||
|
return errors.New("usage: orbit-admin console-user create --credential-file path --username name [--display-name name] [--password-stdin]")
|
||||||
|
}
|
||||||
|
flags := flag.NewFlagSet("console-user create", flag.ContinueOnError)
|
||||||
|
var namespace, credential, credentialFile, username, displayName, password string
|
||||||
|
var passwordStdin bool
|
||||||
|
flags.StringVar(&namespace, "namespace-id", "", "namespace id; can be read from --credential-file")
|
||||||
|
flags.StringVar(&credential, "credential-id", "", "credential id; can be read from --credential-file")
|
||||||
|
flags.StringVar(&credentialFile, "credential-file", "", "private bootstrap credential JSON file")
|
||||||
|
flags.StringVar(&username, "username", "", "console login username")
|
||||||
|
flags.StringVar(&displayName, "display-name", "", "console display name")
|
||||||
|
flags.StringVar(&password, "password", "", "password; prefer --password-stdin")
|
||||||
|
flags.BoolVar(&passwordStdin, "password-stdin", false, "read password from stdin")
|
||||||
|
if err := flags.Parse(args[1:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags.NArg() != 0 || username == "" {
|
||||||
|
return errors.New("--username and principal information are required")
|
||||||
|
}
|
||||||
|
if credentialFile != "" {
|
||||||
|
raw, err := os.ReadFile(credentialFile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("credential file could not be read")
|
||||||
|
}
|
||||||
|
var file struct {
|
||||||
|
NamespaceID core.ID `json:"namespace_id"`
|
||||||
|
CredentialID core.ID `json:"credential_id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &file); err != nil {
|
||||||
|
return errors.New("credential file is invalid")
|
||||||
|
}
|
||||||
|
if namespace == "" {
|
||||||
|
namespace = string(file.NamespaceID)
|
||||||
|
}
|
||||||
|
if credential == "" {
|
||||||
|
credential = string(file.CredentialID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if passwordStdin {
|
||||||
|
raw, err := io.ReadAll(io.LimitReader(os.Stdin, 2049))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("password could not be read")
|
||||||
|
}
|
||||||
|
password = strings.TrimRight(string(raw), "\r\n")
|
||||||
|
}
|
||||||
|
ns, cred := core.ID(namespace), core.ID(credential)
|
||||||
|
if ns.Validate() != nil || cred.Validate() != nil || password == "" {
|
||||||
|
return errors.New("valid namespace, credential and password are required")
|
||||||
|
}
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("database unavailable")
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
user, err := mysql.NewConsoleAuthStore(db).CreateConsoleUser(ctx, mysql.ConsoleUserInput{NamespaceID: ns, CredentialID: cred, Username: username, DisplayName: displayName, Password: password})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(string(encoded))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
108
cmd/orbit-admin/retention.go
Normal file
108
cmd/orbit-admin/retention.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/orbit/internal/config"
|
||||||
|
"git.yuntuo.hk/k2safe/orbit/internal/fault"
|
||||||
|
"git.yuntuo.hk/k2safe/orbit/internal/retention"
|
||||||
|
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||||
|
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runRetentionArchive(args []string) error {
|
||||||
|
flags := flag.NewFlagSet("retention archive", flag.ContinueOnError)
|
||||||
|
var spec retention.JobSpec
|
||||||
|
var directory, namespace string
|
||||||
|
var options retention.JobOptions
|
||||||
|
var timeout time.Duration
|
||||||
|
flags.StringVar(&directory, "directory", os.Getenv("ORBIT_ARCHIVE_DIRECTORY"), "private per-job output directory; same directory resumes saved batches")
|
||||||
|
flags.StringVar(&namespace, "namespace", "", "namespace ID to archive")
|
||||||
|
flags.StringVar(&spec.Kind, "kind", "", "tasks|workflows|events|audit|deliveries|inbox|idempotency")
|
||||||
|
flags.Uint64Var(&spec.PolicyRevision, "revision", 0, "confirmed database retention policy revision")
|
||||||
|
flags.IntVar(&spec.BatchSize, "batch-size", 20, "roots per batch, 1-100; immutable for this job")
|
||||||
|
flags.IntVar(&options.MaxBatches, "max-batches", 10, "maximum new batches per invocation, 1-1000")
|
||||||
|
flags.Int64Var(&options.MaxBytes, "max-bytes", 256<<20, "total saved manifest and batch bytes, 1 MiB-100 GiB")
|
||||||
|
flags.DurationVar(&timeout, "timeout", 5*time.Minute, "invocation deadline, 1s-1h")
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
spec.NamespaceID = core.ID(namespace)
|
||||||
|
if flags.NArg() != 0 || directory == "" || !filepath.IsAbs(directory) || timeout < time.Second || timeout > time.Hour || options.MaxBatches < 1 || options.MaxBatches > 1000 || options.MaxBytes < 1<<20 || options.MaxBytes > 100<<30 {
|
||||||
|
return errors.New("invalid archive arguments; use --help for limits and an absolute --directory")
|
||||||
|
}
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signalCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
ctx, cancel := context.WithTimeout(signalCtx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
db, err := mysql.Open(ctx, cfg.MySQLDSN, 2, 1)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("database unavailable")
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if err := mysql.CheckSchema(ctx, db); err != nil {
|
||||||
|
return errors.New("database schema is not ready")
|
||||||
|
}
|
||||||
|
store := mysql.NewRetentionPolicyStore(db, cfg.RetentionDefaults)
|
||||||
|
status, runErr := retention.RunArchive(ctx, directory, spec, options, func(ctx context.Context, job core.ID, input retention.ArchiveInput) (retention.ArchiveSnapshot, error) {
|
||||||
|
snapshot, err := store.ExportRetentionForJob(ctx, spec.NamespaceID, job, input)
|
||||||
|
if err != nil {
|
||||||
|
// 数据库原始错误可能含 SQL 参数,只输出固定错误码,业务正文不进入控制台日志。
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return retention.ArchiveSnapshot{}, ctx.Err()
|
||||||
|
}
|
||||||
|
return retention.ArchiveSnapshot{}, fmt.Errorf("archive database export failed (%s); keep original directory and verify policy before retrying", fault.Code(err))
|
||||||
|
}
|
||||||
|
return snapshot, nil
|
||||||
|
})
|
||||||
|
if err := json.NewEncoder(os.Stdout).Encode(status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRetentionVerifyArchive(args []string) error {
|
||||||
|
flags := flag.NewFlagSet("retention verify-archive", flag.ContinueOnError)
|
||||||
|
var directory, expectedSHA string
|
||||||
|
flags.StringVar(&directory, "directory", os.Getenv("ORBIT_ARCHIVE_DIRECTORY"), "saved archive job directory; no database connection required")
|
||||||
|
flags.StringVar(&expectedSHA, "expected-sha256", "", "optional independently saved last_sha256 to pin the entire chain")
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags.NArg() != 0 || directory == "" {
|
||||||
|
return errors.New("--directory is required")
|
||||||
|
}
|
||||||
|
if expectedSHA != "" {
|
||||||
|
raw, err := hex.DecodeString(expectedSHA)
|
||||||
|
if err != nil || len(raw) != 32 || hex.EncodeToString(raw) != expectedSHA {
|
||||||
|
return errors.New("--expected-sha256 must be 64 lowercase hexadecimal characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
status, err := retention.VerifyArchive(ctx, directory)
|
||||||
|
if encodeErr := json.NewEncoder(os.Stdout).Encode(status); encodeErr != nil {
|
||||||
|
return encodeErr
|
||||||
|
}
|
||||||
|
if err == nil && expectedSHA != "" && status.LastSHA256 != expectedSHA {
|
||||||
|
return errors.New("archive differs from independently saved digest")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
50
cmd/orbit-admin/retention_test.go
Normal file
50
cmd/orbit-admin/retention_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/orbit/internal/retention"
|
||||||
|
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRetentionArchiveValidationBeforeDatabase(t *testing.T) {
|
||||||
|
t.Setenv("ORBIT_MYSQL_DSN", "")
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{}, {"--directory", "relative"}, {"--directory", t.TempDir(), "--namespace", "invalid"},
|
||||||
|
{"--directory", t.TempDir(), "--namespace", "11111111111111111111111111111111", "--kind", "tasks", "--revision", "1", "--max-batches", "0"},
|
||||||
|
} {
|
||||||
|
if err := runRetentionArchive(args); err == nil {
|
||||||
|
t.Fatalf("invalid args accepted: %v", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyArchiveOfflinePinsDigest(t *testing.T) {
|
||||||
|
t.Setenv("ORBIT_MYSQL_DSN", "")
|
||||||
|
dir := filepath.Join(t.TempDir(), "job")
|
||||||
|
ns := core.ID("11111111111111111111111111111111")
|
||||||
|
spec := retention.JobSpec{NamespaceID: ns, Kind: "tasks", PolicyRevision: 1, BatchSize: 1}
|
||||||
|
status, err := retention.RunArchive(context.Background(), dir, spec, retention.JobOptions{MaxBatches: 1, MaxBytes: 1 << 20}, func(ctx context.Context, id core.ID, input retention.ArchiveInput) (retention.ArchiveSnapshot, error) {
|
||||||
|
policy := retention.Defaults(ns)
|
||||||
|
policy.Enabled = true
|
||||||
|
policy.Revision = 1
|
||||||
|
raw, err := json.Marshal(map[string]any{"policy": policy, "kind": "tasks", "cutoff": time.Now().UTC(), "root_count": 0, "source_retained": true, "next_cursor": ""})
|
||||||
|
digest := sha256.Sum256(raw)
|
||||||
|
return retention.ArchiveSnapshot{FormatVersion: 1, SHA256: hex.EncodeToString(digest[:]), Content: string(raw)}, err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := runRetentionVerifyArchive([]string{"--directory", dir, "--expected-sha256", status.LastSHA256}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := runRetentionVerifyArchive([]string{"--directory", dir, "--expected-sha256", "0000000000000000000000000000000000000000000000000000000000000000"}); err == nil {
|
||||||
|
t.Fatal("trusted digest mismatch accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"git.yuntuo.hk/k2safe/orbit/internal/config"
|
"git.yuntuo.hk/k2safe/orbit/internal/config"
|
||||||
"git.yuntuo.hk/k2safe/orbit/internal/observability"
|
"git.yuntuo.hk/k2safe/orbit/internal/observability"
|
||||||
|
"git.yuntuo.hk/k2safe/orbit/internal/retention"
|
||||||
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||||
"git.yuntuo.hk/k2safe/orbit/internal/transport/httpserver"
|
"git.yuntuo.hk/k2safe/orbit/internal/transport/httpserver"
|
||||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||||
@@ -84,7 +85,9 @@ func run() error {
|
|||||||
txStats := mysql.ReadTransactionStats()
|
txStats := mysql.ReadTransactionStats()
|
||||||
metrics.RecordTransactionStats(observability.TransactionStats{Retries: txStats.Retries, Exhausted: txStats.Exhausted})
|
metrics.RecordTransactionStats(observability.TransactionStats{Retries: txStats.Retries, Exhausted: txStats.Exhausted})
|
||||||
}
|
}
|
||||||
server := httpserver.New(cfg.HTTPAddr, readiness.Check, service.New(mysql.NewTaskStore(db)))
|
consoleAuth := mysql.NewConsoleAuthStore(db)
|
||||||
|
retentionStore := mysql.NewRetentionPolicyStore(db, cfg.RetentionDefaults)
|
||||||
|
server := httpserver.NewWithConsoleAuth(cfg.HTTPAddr, readiness.Check, consoleAuth, service.New(mysql.NewTaskStore(db)))
|
||||||
httpserver.RegisterApplicationResources(server, resources.New(mysql.NewTaskStore(db)))
|
httpserver.RegisterApplicationResources(server, resources.New(mysql.NewTaskStore(db)))
|
||||||
node, err := core.NewID()
|
node, err := core.NewID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,7 +123,38 @@ func run() error {
|
|||||||
httpserver.RegisterSchedules(server, schedule.New(schedules))
|
httpserver.RegisterSchedules(server, schedule.New(schedules))
|
||||||
httpserver.RegisterWorkflowDefinitions(server, definition.New(mysql.NewWorkflowDefinitionStore(db)))
|
httpserver.RegisterWorkflowDefinitions(server, definition.New(mysql.NewWorkflowDefinitionStore(db)))
|
||||||
httpserver.RegisterWorkflows(server, instance.New(mysql.NewWorkflowInstanceStore(db)), control.New(mysql.NewWorkflowControlStore(db)), weavesignal.New(mysql.NewWorkflowSignalStore(db)), recovery.New(mysql.NewWorkflowRecoveryStore(db)))
|
httpserver.RegisterWorkflows(server, instance.New(mysql.NewWorkflowInstanceStore(db)), control.New(mysql.NewWorkflowControlStore(db)), weavesignal.New(mysql.NewWorkflowSignalStore(db)), recovery.New(mysql.NewWorkflowRecoveryStore(db)))
|
||||||
|
httpserver.RegisterConsoleRetention(server, consoleAuth, retentionStore, retentionStore)
|
||||||
|
httpserver.RegisterConsoleArchiveJobs(server, consoleAuth, retentionStore, cfg.ArchiveStorageID)
|
||||||
|
httpserver.WrapConsoleAuth(server, consoleAuth)
|
||||||
httpserver.WrapMetrics(server, metrics)
|
httpserver.WrapMetrics(server, metrics)
|
||||||
|
|
||||||
|
if cfg.ArchiveRoot != "" {
|
||||||
|
archives := &retention.ArchiveRuntime{Directory: cfg.ArchiveRoot, StorageID: cfg.ArchiveStorageID, MaxBytes: cfg.ArchiveMaxBytes, Store: retentionStore}
|
||||||
|
if err := archives.Init(); err != nil {
|
||||||
|
return errors.New("archive volume initialization failed; check path, ownership and storage identity")
|
||||||
|
}
|
||||||
|
archiveCtx, stopArchives := context.WithCancel(root)
|
||||||
|
archiveDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(archiveDone)
|
||||||
|
ticker := time.NewTicker(cfg.ArchiveTickInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-archiveCtx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
call, cancel := context.WithTimeout(archiveCtx, 45*time.Second)
|
||||||
|
err := archives.Tick(call)
|
||||||
|
cancel()
|
||||||
|
if err != nil && archiveCtx.Err() == nil {
|
||||||
|
slog.Error("archive batch could not be confirmed", "node_id", node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() { stopArchives(); <-archiveDone }()
|
||||||
|
}
|
||||||
workflows, err := mysql.NewWorkflowRuntime(db, node)
|
workflows, err := mysql.NewWorkflowRuntime(db, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
58
deploy/docker-compose.ha.yml
Normal file
58
deploy/docker-compose.ha.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# 与 deploy/docker-compose.yml 一起使用:
|
||||||
|
# docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.ha.yml up -d mysql verify orbit-a orbit-b
|
||||||
|
# 默认不启动 base 文件中的 orbit 单节点服务,避免重复实例。
|
||||||
|
|
||||||
|
x-orbit-environment: &orbit-environment
|
||||||
|
ORBIT_HTTP_ADDR: 0.0.0.0:8080
|
||||||
|
ORBIT_MYSQL_DSN: "${ORBIT_DB_USER}:${ORBIT_DB_PASSWORD}@tcp(mysql:3306)/${ORBIT_DB_NAME:-orbit}?parseTime=true&multiStatements=true"
|
||||||
|
ORBIT_DB_MAX_OPEN: ${ORBIT_DB_MAX_OPEN:-20}
|
||||||
|
ORBIT_DB_MAX_IDLE: ${ORBIT_DB_MAX_IDLE:-5}
|
||||||
|
ORBIT_WORKFLOW_TICK_INTERVAL: ${ORBIT_WORKFLOW_TICK_INTERVAL:-1s}
|
||||||
|
ORBIT_SCHEDULE_TICK_INTERVAL: ${ORBIT_SCHEDULE_TICK_INTERVAL:-1s}
|
||||||
|
ORBIT_RECOVERY_TICK_INTERVAL: ${ORBIT_RECOVERY_TICK_INTERVAL:-2s}
|
||||||
|
ORBIT_RUNTIME_BATCH_LIMIT: ${ORBIT_RUNTIME_BATCH_LIMIT:-100}
|
||||||
|
ORBIT_SHUTDOWN_TIMEOUT: ${ORBIT_SHUTDOWN_TIMEOUT:-10s}
|
||||||
|
ORBIT_WORKER_CONNECTION_LIMIT: ${ORBIT_WORKER_CONNECTION_LIMIT:-128}
|
||||||
|
ORBIT_EVENT_CONNECTION_LIMIT: ${ORBIT_EVENT_CONNECTION_LIMIT:-128}
|
||||||
|
ORBIT_DISPATCH_WORKERS: ${ORBIT_DISPATCH_WORKERS:-4}
|
||||||
|
ORBIT_DISPATCH_CAPACITY: ${ORBIT_DISPATCH_CAPACITY:-128}
|
||||||
|
ORBIT_RETENTION_IDEMPOTENCY_DAYS: ${ORBIT_RETENTION_IDEMPOTENCY_DAYS:-90}
|
||||||
|
ORBIT_RETENTION_TASK_DAYS: ${ORBIT_RETENTION_TASK_DAYS:-365}
|
||||||
|
ORBIT_RETENTION_WORKFLOW_DAYS: ${ORBIT_RETENTION_WORKFLOW_DAYS:-365}
|
||||||
|
ORBIT_RETENTION_EVENT_DAYS: ${ORBIT_RETENTION_EVENT_DAYS:-365}
|
||||||
|
ORBIT_RETENTION_DELIVERY_DAYS: ${ORBIT_RETENTION_DELIVERY_DAYS:-90}
|
||||||
|
ORBIT_RETENTION_INBOX_DAYS: ${ORBIT_RETENTION_INBOX_DAYS:-90}
|
||||||
|
ORBIT_RETENTION_AUDIT_DAYS: ${ORBIT_RETENTION_AUDIT_DAYS:-2555}
|
||||||
|
ORBIT_CONSOLE_QUEUE_ID: ${ORBIT_CONSOLE_QUEUE_ID:-}
|
||||||
|
ORBIT_METRICS_ADDR: ${ORBIT_METRICS_ADDR:-}
|
||||||
|
|
||||||
|
services:
|
||||||
|
orbit:
|
||||||
|
profiles:
|
||||||
|
- single
|
||||||
|
|
||||||
|
orbit-a:
|
||||||
|
image: ${ORBIT_IMAGE:-orbit:dev}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
verify:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
<<: *orbit-environment
|
||||||
|
ports:
|
||||||
|
- "${ORBIT_HTTP_BIND:-127.0.0.1}:${ORBIT_HTTP_PORT_A:-8081}:8080"
|
||||||
|
stop_grace_period: 30s
|
||||||
|
|
||||||
|
orbit-b:
|
||||||
|
image: ${ORBIT_IMAGE:-orbit:dev}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
verify:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
<<: *orbit-environment
|
||||||
|
ports:
|
||||||
|
- "${ORBIT_HTTP_BIND:-127.0.0.1}:${ORBIT_HTTP_PORT_B:-8082}:8080"
|
||||||
|
stop_grace_period: 30s
|
||||||
102
deploy/docker-compose.yml
Normal file
102
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
name: orbit
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.4
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${ORBIT_DB_NAME:-orbit}
|
||||||
|
MYSQL_USER: ${ORBIT_DB_USER:?ORBIT_DB_USER is required}
|
||||||
|
MYSQL_PASSWORD: ${ORBIT_DB_PASSWORD:?ORBIT_DB_PASSWORD is required}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${ORBIT_DB_ROOT_PASSWORD:?ORBIT_DB_ROOT_PASSWORD is required}
|
||||||
|
volumes:
|
||||||
|
- orbit-mysql:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- MYSQL_PWD="$${MYSQL_ROOT_PASSWORD}" mysqladmin ping -h 127.0.0.1 -uroot --silent
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
image: ${ORBIT_IMAGE:-orbit:dev}
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: ["/usr/local/bin/orbit-migrate"]
|
||||||
|
command: ["up"]
|
||||||
|
environment:
|
||||||
|
ORBIT_MYSQL_DSN: "root:${ORBIT_DB_ROOT_PASSWORD}@tcp(mysql:3306)/${ORBIT_DB_NAME:-orbit}?parseTime=true&multiStatements=true"
|
||||||
|
|
||||||
|
grant:
|
||||||
|
image: mysql:8.4
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: ["/bin/sh", "-ec"]
|
||||||
|
command:
|
||||||
|
- |
|
||||||
|
case "$${ORBIT_DB_NAME}" in *[!A-Za-z0-9_]*|'') echo 'ORBIT_DB_NAME must contain only letters, numbers and underscores' >&2; exit 2;; esac
|
||||||
|
case "$${ORBIT_DB_USER}" in *[!A-Za-z0-9_]*|'') echo 'ORBIT_DB_USER must contain only letters, numbers and underscores' >&2; exit 2;; esac
|
||||||
|
MYSQL_PWD="$${MYSQL_ROOT_PASSWORD}" mysql --protocol=tcp -h mysql -uroot \
|
||||||
|
-e "REVOKE ALL PRIVILEGES, GRANT OPTION FROM '$${ORBIT_DB_USER}'@'%'; \
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON \`$${ORBIT_DB_NAME}\`.* TO '$${ORBIT_DB_USER}'@'%'; \
|
||||||
|
FLUSH PRIVILEGES;"
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${ORBIT_DB_ROOT_PASSWORD:?ORBIT_DB_ROOT_PASSWORD is required}
|
||||||
|
ORBIT_DB_NAME: ${ORBIT_DB_NAME:-orbit}
|
||||||
|
ORBIT_DB_USER: ${ORBIT_DB_USER:?ORBIT_DB_USER is required}
|
||||||
|
|
||||||
|
verify:
|
||||||
|
image: ${ORBIT_IMAGE:-orbit:dev}
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
grant:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
entrypoint: ["/usr/local/bin/orbit-admin"]
|
||||||
|
command: ["verify"]
|
||||||
|
environment:
|
||||||
|
ORBIT_MYSQL_DSN: "${ORBIT_DB_USER}:${ORBIT_DB_PASSWORD}@tcp(mysql:3306)/${ORBIT_DB_NAME:-orbit}?parseTime=true&multiStatements=true"
|
||||||
|
|
||||||
|
orbit:
|
||||||
|
image: ${ORBIT_IMAGE:-orbit:dev}
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
depends_on:
|
||||||
|
verify:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
ORBIT_HTTP_ADDR: 0.0.0.0:8080
|
||||||
|
ORBIT_MYSQL_DSN: "${ORBIT_DB_USER}:${ORBIT_DB_PASSWORD}@tcp(mysql:3306)/${ORBIT_DB_NAME:-orbit}?parseTime=true&multiStatements=true"
|
||||||
|
ORBIT_DB_MAX_OPEN: ${ORBIT_DB_MAX_OPEN:-20}
|
||||||
|
ORBIT_DB_MAX_IDLE: ${ORBIT_DB_MAX_IDLE:-5}
|
||||||
|
ORBIT_WORKFLOW_TICK_INTERVAL: ${ORBIT_WORKFLOW_TICK_INTERVAL:-1s}
|
||||||
|
ORBIT_SCHEDULE_TICK_INTERVAL: ${ORBIT_SCHEDULE_TICK_INTERVAL:-1s}
|
||||||
|
ORBIT_RECOVERY_TICK_INTERVAL: ${ORBIT_RECOVERY_TICK_INTERVAL:-2s}
|
||||||
|
ORBIT_RUNTIME_BATCH_LIMIT: ${ORBIT_RUNTIME_BATCH_LIMIT:-100}
|
||||||
|
ORBIT_SHUTDOWN_TIMEOUT: ${ORBIT_SHUTDOWN_TIMEOUT:-10s}
|
||||||
|
ORBIT_WORKER_CONNECTION_LIMIT: ${ORBIT_WORKER_CONNECTION_LIMIT:-128}
|
||||||
|
ORBIT_EVENT_CONNECTION_LIMIT: ${ORBIT_EVENT_CONNECTION_LIMIT:-128}
|
||||||
|
ORBIT_DISPATCH_WORKERS: ${ORBIT_DISPATCH_WORKERS:-4}
|
||||||
|
ORBIT_DISPATCH_CAPACITY: ${ORBIT_DISPATCH_CAPACITY:-128}
|
||||||
|
ORBIT_RETENTION_IDEMPOTENCY_DAYS: ${ORBIT_RETENTION_IDEMPOTENCY_DAYS:-90}
|
||||||
|
ORBIT_RETENTION_TASK_DAYS: ${ORBIT_RETENTION_TASK_DAYS:-365}
|
||||||
|
ORBIT_RETENTION_WORKFLOW_DAYS: ${ORBIT_RETENTION_WORKFLOW_DAYS:-365}
|
||||||
|
ORBIT_RETENTION_EVENT_DAYS: ${ORBIT_RETENTION_EVENT_DAYS:-365}
|
||||||
|
ORBIT_RETENTION_DELIVERY_DAYS: ${ORBIT_RETENTION_DELIVERY_DAYS:-90}
|
||||||
|
ORBIT_RETENTION_INBOX_DAYS: ${ORBIT_RETENTION_INBOX_DAYS:-90}
|
||||||
|
ORBIT_RETENTION_AUDIT_DAYS: ${ORBIT_RETENTION_AUDIT_DAYS:-2555}
|
||||||
|
ORBIT_CONSOLE_QUEUE_ID: ${ORBIT_CONSOLE_QUEUE_ID:-}
|
||||||
|
ORBIT_METRICS_ADDR: ${ORBIT_METRICS_ADDR:-}
|
||||||
|
ports:
|
||||||
|
- "${ORBIT_HTTP_BIND:-127.0.0.1}:${ORBIT_HTTP_PORT:-8080}:8080"
|
||||||
|
stop_grace_period: 30s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
orbit-mysql:
|
||||||
122
deploy/nginx/orbit.conf.example
Normal file
122
deploy/nginx/orbit.conf.example
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Orbit HTTPS/WSS 入口示例。
|
||||||
|
# 将 orbit.example.com 和证书路径替换成正式值后,放到 Nginx 的 conf.d 目录。
|
||||||
|
# 本文件位于 http 配置上下文中;map/limit_req_zone 不能放进 server 块。
|
||||||
|
|
||||||
|
map $http_upgrade $orbit_connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
limit_req_zone $binary_remote_addr zone=orbit_console_login:10m rate=10r/m;
|
||||||
|
|
||||||
|
upstream orbit_backend {
|
||||||
|
server 127.0.0.1:8080;
|
||||||
|
keepalive 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name orbit.example.com;
|
||||||
|
|
||||||
|
# ACME HTTP-01 或其他证书签发工具可使用此目录。
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/orbit-acme;
|
||||||
|
default_type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 308 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name orbit.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/orbit.example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/orbit.example.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:orbit_tls:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
client_max_body_size 1m;
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
|
||||||
|
# 长连接只用于 Worker 和结果订阅;心跳由协议负责,代理不能自行关闭空闲会话。
|
||||||
|
location = /v1/worker/connect {
|
||||||
|
proxy_pass http://orbit_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $orbit_connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /v1/events/connect {
|
||||||
|
proxy_pass http://orbit_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $orbit_connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 控制台登录额外限制来源 IP 的尝试速率;业务任务速率仍由 Orbit 的持久化配额和授权决定。
|
||||||
|
location = /console/auth/login {
|
||||||
|
limit_req zone=orbit_console_login burst=5 nodelay;
|
||||||
|
proxy_pass http://orbit_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /health/live {
|
||||||
|
access_log off;
|
||||||
|
proxy_pass http://orbit_backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /health/ready {
|
||||||
|
access_log off;
|
||||||
|
proxy_pass http://orbit_backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://orbit_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 数据模型与实现基线
|
# 数据模型与实现基线
|
||||||
|
|
||||||
更新:2026-09-18。数据库统一禁止外键。当前迁移版本为28;000026–000028只增加固定业务观测读取所需索引,不增加实体或字段。当前已实现任务执行、安全重试、运行中取消、计划调度、队列限速/背压和应用配额;优先级及节点公平领取已实现;可靠订阅核心、HTTP/WSS、管理与消费 SDK 已实现,独立 Inbox 场景已实现,流程编排继续开发。
|
更新:2026-09-22。数据库统一禁止外键。当前迁移版本为30;000026–000028只增加固定业务观测读取所需索引,000029增加控制台账号/会话,000030增加命名空间级保留策略。当前已实现任务执行、安全重试、运行中取消、计划调度、队列限速/背压和应用配额;优先级及节点公平领取已实现;可靠订阅核心、HTTP/WSS、管理与消费 SDK 已实现,独立 Inbox 场景已实现,流程编排继续开发。
|
||||||
|
|
||||||
## 1. 模块与依赖
|
## 1. 模块与依赖
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
## 3. 实体清单
|
## 3. 实体清单
|
||||||
|
|
||||||
当前共 36 个实体:初始迁移包含 30 个,000003 增加进程会话头和迟到证据,000010 增加应用共享额度,000013 增加结果流与不可变结果快照。000024 增加固定流程版本的查询范围。迁移自身另有 `orbit_schema_migrations` 记录。
|
当前共 39 个业务实体:原运行模型为36个,000029增加控制台账号与会话,000030增加保留策略。迁移自身另有 `orbit_schema_migrations` 记录;控制台和策略表同样遵循零外键、中文 COMMENT、应用层归属校验。
|
||||||
|
|
||||||
| 模块 | Go 模型 | 数据表 |
|
| 模块 | Go 模型 | 数据表 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
| platform | `Subscription` | `orbit_subscriptions` |
|
| platform | `Subscription` | `orbit_subscriptions` |
|
||||||
| platform | `Delivery` | `orbit_deliveries` |
|
| platform | `Delivery` | `orbit_deliveries` |
|
||||||
| platform | `AuditEvent` | `orbit_audit_events` |
|
| platform | `AuditEvent` | `orbit_audit_events` |
|
||||||
|
| platform | `ConsoleUser` | `orbit_console_users` |
|
||||||
|
| platform | `ConsoleSession` | `orbit_console_sessions` |
|
||||||
|
| internal/retention | `Policy` | `orbit_retention_policies` |
|
||||||
| pulse | `Queue` | `pulse_queues` |
|
| pulse | `Queue` | `pulse_queues` |
|
||||||
| pulse | `ExecutionGate` | `pulse_execution_gates` |
|
| pulse | `ExecutionGate` | `pulse_execution_gates` |
|
||||||
| pulse | `WorkerSession` | `pulse_worker_sessions` |
|
| pulse | `WorkerSession` | `pulse_worker_sessions` |
|
||||||
@@ -126,7 +129,7 @@ P2 服务端已新增会话隔离、两步派发、租约、并发许可、结
|
|||||||
|
|
||||||
000007_task_cancellation 允许尝试证据记录 CANCELLED,不新增实体;取消意图、结果和历史不确定效果的规则见 [取消契约](api/cancellation.md)。
|
000007_task_cancellation 允许尝试证据记录 CANCELLED,不新增实体;取消意图、结果和历史不确定效果的规则见 [取消契约](api/cancellation.md)。
|
||||||
|
|
||||||
000008_no_foreign_keys 移除历史 70 个外键,保留索引、唯一约束、CHECK、触发器和数据。旧迁移已在回归库应用,按不可变校验和约定保留原文件;已有库继续按历史链前向升级。新装库由迁移器使用当前版本的无外键基线直接建表,再登记 1–28 的不可变校验和,因此初始化过程也不会先创建历史外键;基线版本变化时必须随新迁移重新生成并回归。后续迁移禁止再引入外键。升级旧库时先停用旧服务,应用完整迁移再启动新版;迁移需要 DDL 锁,应在目标部署环境演练维护窗口。
|
000008_no_foreign_keys 移除历史 70 个外键,保留索引、唯一约束、CHECK、触发器和数据。旧迁移已在回归库应用,按不可变校验和约定保留原文件;已有库继续按历史链前向升级。新装库由迁移器使用当前版本的无外键基线直接建表,再登记 1–30 的不可变校验和,因此初始化过程也不会先创建历史外键;基线版本变化时必须随新迁移重新生成并回归。后续迁移禁止再引入外键。升级旧库时先停用旧服务,应用完整迁移再启动新版;迁移需要 DDL 锁,应在目标部署环境演练维护窗口。
|
||||||
|
|
||||||
000009_queue_resources 增加 Queue.MaxUnfinishedTasks、UnfinishedTasks、StartWindowUntil、StartsInWindow,Go 字段与 SQL COMMENT 均有中文语义,实体仍为 32。未终结任务按 WAITING/LEASED/RUNNING/NEEDS_REVIEW 回填;窗口初始为空、计数为零。运行计数与任务/开始授权同事务维护,不改变队列配置 revision,配置变更不能重置它们。降额允许已有使用量暂时超过新上限,不删除任务或撤销已有许可;详见 [队列契约](api/queues.md) 和 [资源回归](reports/phase-3-queue-resources.md)。
|
000009_queue_resources 增加 Queue.MaxUnfinishedTasks、UnfinishedTasks、StartWindowUntil、StartsInWindow,Go 字段与 SQL COMMENT 均有中文语义,实体仍为 32。未终结任务按 WAITING/LEASED/RUNNING/NEEDS_REVIEW 回填;窗口初始为空、计数为零。运行计数与任务/开始授权同事务维护,不改变队列配置 revision,配置变更不能重置它们。降额允许已有使用量暂时超过新上限,不删除任务或撤销已有许可;详见 [队列契约](api/queues.md) 和 [资源回归](reports/phase-3-queue-resources.md)。
|
||||||
|
|
||||||
@@ -253,3 +256,21 @@ Event.WorkflowID为只读STORED生成列,从原不可变事件派生流程归
|
|||||||
000027为`orbit_events(type,occurred_at,id)`增加固定事件窗口索引。采样器只读取代码列出的任务和流程事件类别、最近5分钟、每类最多10000条;未知事件类型不写入标签,事件指标只说明本节点有限趋势,不能代替审计查询、端到端阶段延迟或跨节点吞吐。索引不改变事件原文、不可变触发器、外键策略或事务边界。
|
000027为`orbit_events(type,occurred_at,id)`增加固定事件窗口索引。采样器只读取代码列出的任务和流程事件类别、最近5分钟、每类最多10000条;未知事件类型不写入标签,事件指标只说明本节点有限趋势,不能代替审计查询、端到端阶段延迟或跨节点吞吐。索引不改变事件原文、不可变触发器、外键策略或事务边界。
|
||||||
|
|
||||||
000028为`pulse_tasks(state,completed_at,id)`和`weave_instances(state,completed_at,id)`增加终态耗时样本索引。采样器只读取最近5分钟内成功、失败或取消记录,最多10000条,计算创建到终态的平均/最大秒数并在截断时复用统一标记;不把该样本解释为端到端外部效果或全局SLO。
|
000028为`pulse_tasks(state,completed_at,id)`和`weave_instances(state,completed_at,id)`增加终态耗时样本索引。采样器只读取最近5分钟内成功、失败或取消记录,最多10000条,计算创建到终态的平均/最大秒数并在截断时复用统一标记;不把该样本解释为端到端外部效果或全局SLO。
|
||||||
|
|
||||||
|
## 控制台身份与历史保留策略(000029–000030)
|
||||||
|
|
||||||
|
000029增加 `orbit_console_users` 和 `orbit_console_sessions`,只保存控制台账号的 bcrypt 哈希、绑定凭据及 HttpOnly 会话摘要;不保存密码或令牌原文。控制台继续单管理员模型,服务层每次修改前核对会话、命名空间和绑定凭据权限。
|
||||||
|
|
||||||
|
000030增加 `orbit_retention_policies`,按命名空间保存归档开关、幂等/任务/流程/事件/交付/Inbox/审计窗口、修订号及时间。所有字段都有中文 COMMENT,策略不含外键或级联。数据库策略优先于部署默认值;默认关闭归档,配置变化不触发删除。服务层用同一事务完成版本核对、幂等回执和审计快照,并在归档预览/导出时保护未确认交付、未知效果、未结束流程和有效幂等身份。具体窗口、候选目录和控制台接口见[数据保留与归档策略](api/retention.md)。
|
||||||
|
|
||||||
|
## 文件归档作业模型
|
||||||
|
|
||||||
|
`internal/retention` 中的 `JobSpec`、`JobManifest`、`JobBatch` 和 `JobStatus` 定义固定作业范围、不可变清单、连续批次及汇总状态;各持久字段提供中文注释。它们只存入私有归档目录,不新增数据库表。连续文件与摘要链就是恢复检查点,作业 `JobID` 进入现有管理审计的执行者身份。协议与失败边界见[文件归档作业](api/retention-jobs.md)。
|
||||||
|
|
||||||
|
### 周期归档(000031)
|
||||||
|
|
||||||
|
`orbit_archive_plans` 以命名空间和类别唯一,保存配置版本、保留策略版本、卷身份、周期秒数、批大小、字节预算、下次触发时间和未结清作业占位。失败也占位,关闭计划只停止新作业。
|
||||||
|
|
||||||
|
`orbit_archive_jobs` 固定范围和卷,保存 PENDING/RUNNING/SUCCEEDED/FAILED/ABANDONED 状态、版本、领取次数、已确认的批数/根数/字节/链摘要,以及随机租约代次和截止。租约确认必须匹配代次与版本且未过期;人工操作需要原版本、幂等键与审计原因。两个表均无外键,关联在同一事务按命名空间核验。
|
||||||
|
|
||||||
|
数据库与文件不做跨资源事务。持久文件是恢复依据,数据库链前缀是可信核验锚点;文件发布后确认丢失,由原作业重新读文件校验后收敛,缺失文件不自动重建。见[接口与部署说明](api/archive-scheduler.md)。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Orbit 持续开发与回归计划
|
# Orbit 持续开发与回归计划
|
||||||
|
|
||||||
更新:2026-09-20。此计划对应持续开发目标,由开发过程同步维护。数据库已按用户要求采用无外键设计,迁移 000008 移除历史外键,关联一致性由服务端事务保证。
|
更新:2026-09-24。此计划对应持续开发目标,由开发过程同步维护。数据库已按用户要求采用无外键设计,迁移 000008 移除历史外键,关联一致性由服务端事务保证。
|
||||||
|
|
||||||
## 1. 最终交付目标
|
## 1. 最终交付目标
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
仓库内的开发回归统一入口为 `make release-local-check`,它只负责可重复的本地门禁;真实 MySQL 全量回归使用 `make integration`,目标拓扑生产验收仍按交付清单单独留证。
|
仓库内的开发回归统一入口为 `make release-local-check`,它只负责可重复的本地门禁;真实 MySQL 全量回归使用 `make integration`,目标拓扑生产验收仍按交付清单单独留证。
|
||||||
|
|
||||||
## 1.1 当前交付快照(2026-09-20)
|
## 1.1 当前交付快照(2026-09-21)
|
||||||
|
|
||||||
当前不是“还剩一半代码”的状态,而是核心平台已经进入生产验收阶段。为避免把代码完成度和上线完成度混在一起,按三层记录:
|
当前不是“还剩一半代码”的状态,而是核心平台已经进入生产验收阶段。为避免把代码完成度和上线完成度混在一起,按三层记录:
|
||||||
|
|
||||||
@@ -32,7 +32,29 @@
|
|||||||
|
|
||||||
### 首个远端接入版本
|
### 首个远端接入版本
|
||||||
|
|
||||||
本批按用户要求发布仓库 `main` 及 `v0.1.0` 接入版本,独立 SDK 使用标签 `sdk/go/v0.1.0`,供业务项目直接 `go get`。同步移除手册中“尚未发布、必须本地替换”的接入说明,发布后在仓库外使用空模块缓存验证匿名下载、固定版本、无 `replace` 构建与校验和,记录实际远端验证结果。该交付不关闭上方目标环境及完整跨版本生产验收项。
|
已按用户要求推送仓库 `main` 及 `v0.1.0` 接入版本,独立 SDK 标签为 `sdk/go/v0.1.0`,供业务项目直接 `go get`。两个标签固定提交 `0e8b2ef2975df6b55f2240dd14eda83adf30ea94`;仓库外空模块缓存下载、固定版本、无 `replace` 编译、HTTP smoke 和校验和验证已通过,见[远端发布报告](reports/phase-7-sdk-remote-release.md)。本机验证沿用系统网络代理及公开 Go 模块代理,保持 sumdb 校验。该交付不关闭上方目标环境及完整跨版本生产验收项。
|
||||||
|
|
||||||
|
### 控制台第一版
|
||||||
|
|
||||||
|
已将 [Orbit Console](console.md) 编译进服务端,入口为 `/console/`。第一版读取健康、版本、任务、计划、流程和结果订阅接口,使用数据库控制台账号和 HttpOnly 会话,响应式总览和有界自动刷新;不直接访问数据库,也不绕过现有凭据与动作范围。首个管理员可以随 `orbit-admin bootstrap` 同事务创建,独立管理员身份和跨命名空间聚合仍不在控制台内提供。
|
||||||
|
|
||||||
|
控制台任务详情已接入 Pulse 的任务快照、执行尝试、操作审计、取消和安全重试接口。页面操作沿用 `expected_revision`、`Idempotency-Key`、原因、证据和服务端权限校验;`NEEDS_REVIEW` 仍要求通过核查 API/SDK 提交最终结论。流程实例详情已接入当前视图、步骤状态和有界事件时间线,并支持带 `control_revision` 的暂停、恢复和取消;对 `NEEDS_REVIEW` 实例进一步提供六类带证据的恢复入口(任务核查、步骤结论、继续、步骤重试、PENDING 接续、等待重建),页面只组装协议请求,当前修订、激活身份、权限、审计和状态门控由服务端负责。结果订阅详情已接入配置、积压窗口和交付历史,并支持启停及 EXPIRED 交付追加预算重试。权限不足时保留明确提示,不把页面操作视为绕过服务端事务边界的快捷入口。
|
||||||
|
|
||||||
|
定时计划详情已接入当前版本、规则、下一次触发和 occurrence 历史;暂停/恢复沿用 `schedules:manage`、`expected_revision`、原因和幂等键。计划发布、跨命名空间管理身份和更高风险的交付控制仍按独立权限模型推进。
|
||||||
|
|
||||||
|
控制台六个核心模块已拆为独立 hash 视图,导航状态和页面标题随视图同步,刷新或直接打开对应 hash 后仍保持正确模块。数据请求继续由统一会话和当前命名空间范围驱动。
|
||||||
|
|
||||||
|
已完成一次真实链路回归:标准 `scripts/test-mysql.sh` 在一次性 MySQL 和真实 Orbit 进程中跑通任务、Worker、结果订阅、Weave 流程、进程恢复及独立业务示例;控制台前五个入口、健康检查和详情所依赖的读取 API 已用真实结果核对;第六个保留策略入口另有真实 MySQL、首次部署 HTTP 与浏览器操作回归,证据见 [Console 真实链路回归](reports/phase-7-console-e2e.md)。这关闭了本地闭环验证项,生产拓扑和发布门槛仍保持未验收。
|
||||||
|
|
||||||
|
新增 `make console-e2e` 作为首次部署闭环门禁:在一次性 MySQL 和 Docker Orbit 中从空库迁移、初始化控制台管理员,完成登录、受保护页面、WS Worker 成功执行、明确失败重试、定时任务、控制台任务读取和登出后的 401 校验,证据见[首次部署与控制台任务闭环](reports/phase-8-console-e2e.md)。该门禁证明功能链路可重复,不替代目标生产拓扑、容量和 RPO/RTO 验收。
|
||||||
|
|
||||||
|
新增 `deploy/docker-compose.yml` 作为单节点可部署基线:MySQL 健康检查通过后依次执行迁移和 `orbit-admin verify`,核验成功才启动 Orbit;`make compose-check` 提供不泄露密码的静态配置门禁,首次管理员初始化和域名/同源访问说明见[Compose 部署基线](22-compose-deployment.md),实际正常启动与失败阻断证据见[Compose 部署基线验证](reports/phase-9-compose-deployment.md)。该基线不替代生产数据库高可用、正式 Secret、容量、备份和 RPO/RTO 验收。
|
||||||
|
|
||||||
|
新增 `deploy/nginx/orbit.conf.example` 作为可选 HTTPS/WSS 入口模板:Compose 默认只将业务端口绑定到 `127.0.0.1`,Nginx 负责 TLS 终止、WebSocket 升级、健康探针代理和控制台登录来源限速;配置语法及端口边界证据见[HTTPS/WSS 入口基线验证](reports/phase-10-ingress.md)。Nginx 是部署入口,不会成为 Orbit 服务端的业务依赖,真实域名、正式证书、入口故障和公网重连仍需目标环境验收。
|
||||||
|
|
||||||
|
新增 `deploy/docker-compose.ha.yml` 作为两节点隔离验收覆盖:节点共享持久化 MySQL 状态但使用独立 HTTP 端口,base 服务中的单节点 Orbit 通过 profile 关闭;两个 `ready` 节点均通过后才能接入入口,滚动停止和重新拉起按[Compose 部署基线](22-compose-deployment.md)执行。该覆盖不替代跨故障域节点、数据库自动切换、负载均衡器和目标容量验收。
|
||||||
|
|
||||||
|
两节点覆盖已完成一次真实 Docker 回归:空库迁移和权限核验后两个节点均就绪,停止 A 后 B 保持就绪,A 重新拉起并重新核验后恢复;证据见[两节点 Compose 覆盖验证](reports/phase-11-two-node-compose.md)。该结果只关闭单机双进程启动/滚动动作,不关闭生产 HA。
|
||||||
|
|
||||||
## 2. 持续推进方式
|
## 2. 持续推进方式
|
||||||
|
|
||||||
@@ -56,8 +78,8 @@
|
|||||||
| P3 Pulse 完整能力 | 安全重试、延时、Cron/固定间隔、漏跑策略、计划版本、运行中取消、并发许可、配额/背压、可靠结果订阅、批量受理、独立任务核查/人工重试、查询筛选与事务Outbox接入 | 多实例领取/触发竞争、重试上限、取消竞争、队列隔离、积压恢复、事件重复交付 | 已完成平台功能阶段验收:[原范围逐项核对](reports/phase-3.md)、[本轮完整回归](reports/phase-3-delay-recovery.md)。补齐相对延迟、改参数关联示例和四种重试全停恢复场景;20个可运行示例全部通过。全平台生产门槛及SDK首版交付剩余项仍按P6–P7推进 |
|
| P3 Pulse 完整能力 | 安全重试、延时、Cron/固定间隔、漏跑策略、计划版本、运行中取消、并发许可、配额/背压、可靠结果订阅、批量受理、独立任务核查/人工重试、查询筛选与事务Outbox接入 | 多实例领取/触发竞争、重试上限、取消竞争、队列隔离、积压恢复、事件重复交付 | 已完成平台功能阶段验收:[原范围逐项核对](reports/phase-3.md)、[本轮完整回归](reports/phase-3-delay-recovery.md)。补齐相对延迟、改参数关联示例和四种重试全停恢复场景;20个可运行示例全部通过。全平台生产门槛及SDK首版交付剩余项仍按P6–P7推进 |
|
||||||
| P4 Weave 主流程 | SDK 定义构建与发布校验、实例、顺序/分支/结构化并行汇合、信号/定时等待、版本绑定 | 重启接续、早到/重复信号、并行竞争、内部事件重放、发布版本兼容;端到端通用流程 | 已完成平台功能阶段验收:[原功能及W01–W17逐项核对](reports/phase-4-5.md)、[版本/暂停/全停回归](reports/phase-4-5-races.md)、[提交故障边界](reports/phase-6-fault-boundaries.md)。研发发布、固定版本、顺序/分支/有限并行、信号/时间等待、公共API/SDK及可运行示例均已交付;生产拓扑与发布兼容继续P6/P7 |
|
| P4 Weave 主流程 | SDK 定义构建与发布校验、实例、顺序/分支/结构化并行汇合、信号/定时等待、版本绑定 | 重启接续、早到/重复信号、并行竞争、内部事件重放、发布版本兼容;端到端通用流程 | 已完成平台功能阶段验收:[原功能及W01–W17逐项核对](reports/phase-4-5.md)、[版本/暂停/全停回归](reports/phase-4-5-races.md)、[提交故障边界](reports/phase-6-fault-boundaries.md)。研发发布、固定版本、顺序/分支/有限并行、信号/时间等待、公共API/SDK及可运行示例均已交付;生产拓扑与发布兼容继续P6/P7 |
|
||||||
| P5 Weave 恢复与查询 | 有界查询、暂停继续、取消、人工核查恢复、控制门控与审计 | 轮次与尝试独立预算;暂停/开始/回执/超时竞争;恢复保留预算;未知结果不盲目重复 | 已完成平台功能阶段验收:[原功能核对](reports/phase-4-5.md)。有界查询、独立预算、暂停继续/取消、六类恢复、历史/筛选/事件/来源与SDK场景均已交付;真实竞争、COMMIT断线及进程全停保留原身份、截止和预算。运维、容量与首版发布继续P6/P7 |
|
| P5 Weave 恢复与查询 | 有界查询、暂停继续、取消、人工核查恢复、控制门控与审计 | 轮次与尝试独立预算;暂停/开始/回执/超时竞争;恢复保留预算;未知结果不盲目重复 | 已完成平台功能阶段验收:[原功能核对](reports/phase-4-5.md)。有界查询、独立预算、暂停继续/取消、六类恢复、历史/筛选/事件/来源与SDK场景均已交付;真实竞争、COMMIT断线及进程全停保留原身份、截止和预算。运维、容量与首版发布继续P6/P7 |
|
||||||
| P6 生产验证与运维 | 完整监控、凭据轮换、备份恢复、滚动升级、运行手册、可复现负载和故障工具 | 对照生产验收清单执行,报告环境/指标/遗留项;容量、RPO/RTO 按真实拓扑验证 | 进行中;已补[精确故障边界](reports/phase-6-fault-boundaries.md)和[进程观测](reports/phase-6-observability.md);继续业务积压/阶段延迟、凭据轮换及其余生产门槛 |
|
| P6 生产验证与运维 | 完整监控、凭据轮换、备份恢复、滚动升级、运行手册、可复现负载和故障工具 | 对照生产验收清单执行,报告环境/指标/遗留项;容量、RPO/RTO 按真实拓扑验证 | 进行中;已补[精确故障边界](reports/phase-6-fault-boundaries.md)、[进程观测](reports/phase-6-observability.md)、数据库短暂不可用、备份恢复、GTID 手工提升和定时积压样本;继续业务积压/阶段延迟、凭据轮换及其余生产门槛 |
|
||||||
| P7 首版交付核对 | SDK 兼容与发布准备、完整 API 手册、场景示例、全量回归及交付清单 | 新接入者按文档跑通全部示例;所有首版能力可追溯到实现与测试;报告实际生产边界 | 进行中;已补[通用文件转换示例](reports/phase-7-file-conversion.md)、[SDK独立发布门禁](reports/phase-7-sdk-release.md)、[当前协议兼容矩阵](reports/phase-7-compatibility.md)、[历史任务 API 双向演练](reports/phase-7-historical-compatibility.md)、[当前回归状态](reports/phase-7-regression-status.md)、[首版交付核对清单](release-checklist.md)、[发布物身份清单](api/operations.md#发布物身份清单)和[容器发布说明](21-container-deployment.md),镜像已完成 linux/amd64 构建验证,节点版本身份可由 `/version` 核对;`verify --manifest` 已可在数据库核验前绑定候选二进制身份;仍需带标签的完整跨版本矩阵和目标拓扑验收 |
|
| P7 首版交付核对 | SDK 兼容与发布准备、完整 API 手册、场景示例、全量回归及交付清单 | 新接入者按文档跑通全部示例;所有首版能力可追溯到实现与测试;报告实际生产边界 | 进行中;已补[通用文件转换示例](reports/phase-7-file-conversion.md)、[SDK独立发布门禁](reports/phase-7-sdk-release.md)、[当前协议兼容矩阵](reports/phase-7-compatibility.md)、[历史任务 API 双向演练](reports/phase-7-historical-compatibility.md)、[当前回归状态](reports/phase-7-regression-status.md)、[四项闭环验收](reports/phase-7-acceptance-20260921.md)、[首版交付核对清单](release-checklist.md)、[发布物身份清单](api/operations.md#发布物身份清单)和[容器发布说明](21-container-deployment.md),镜像已完成 linux/amd64 构建验证,节点版本身份可由 `/version` 核对;`verify --manifest` 已可在数据库核验前绑定候选二进制身份;仍需带标签的完整跨版本矩阵和目标拓扑验收 |
|
||||||
|
|
||||||
SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最后才开始补文档。
|
SDK、文档与示例随 P1–P5 同步开发,P7 是全量核对,不是最后才开始补文档。
|
||||||
|
|
||||||
@@ -85,10 +107,10 @@ P4/P5定时等待与后续查询轮次的实现及回归记录见[时钟报告](
|
|||||||
### P6 首批实施顺序
|
### P6 首批实施顺序
|
||||||
|
|
||||||
1. 已补[首批可复现故障工具](reports/phase-6-fault-boundaries.md):A04“派发已提交但消息尚未送达”、W01/W13事务提交结果不明,以及W16最终回执/旧PENDING结果重放/下一轮竞争。迁移器同时提供当前版本无外键新装基线;每个切点必须有实际数据库或进程证据,不能用响应错误推断事务是否已提交,也不能用普通SIGNAL竞争替代最终查询场景。数据库事务封装另对提交前 MySQL 1205/1213 做最多两次短退避重试,Commit 回执不明保持原身份核对语义,真实锁环回归见[P7当前回归状态](reports/phase-7-regression-status.md)。
|
1. 已补[首批可复现故障工具](reports/phase-6-fault-boundaries.md):A04“派发已提交但消息尚未送达”、W01/W13事务提交结果不明,以及W16最终回执/旧PENDING结果重放/下一轮竞争。迁移器同时提供当前版本无外键新装基线;每个切点必须有实际数据库或进程证据,不能用响应错误推断事务是否已提交,也不能用普通SIGNAL竞争替代最终查询场景。数据库事务封装另对提交前 MySQL 1205/1213 做最多两次短退避重试,Commit 回执不明保持原身份核对语义,真实锁环回归见[P7当前回归状态](reports/phase-7-regression-status.md)。
|
||||||
2. 已实现[基础运行观测](api/observability.md)及[有界业务状态与事件采样](reports/phase-6-business-observability.md):HTTP/WS、数据库连接池、后台批次、固定任务/交付/流程/Inbox状态计数、最近事件类别、终态耗时样本、固定事件阶段耗时、最早年龄、截断/新鲜度指标、提交前 MySQL 事务重试/耗尽计数和可离线验证的告警规则;独立采集监听默认关闭,固定指标目录拒绝动态标签;发布前只读结构核验由[`orbit-admin verify`](api/operations.md)提供,运维人员也可用 `orbit-admin observe snapshot` 获取同一有界单节点快照;后台循环间隔、批量上限、Worker/Event连接上限、Hub领取协程和有界内存排队均支持有界环境配置,并在启动日志和固定指标中记录。仍需端到端外部效果延迟、全局持久吞吐、Inbox从事件产生到推进完成的精确阶段延迟、全局发送队列、多节点聚合、锁等待/死锁告警阈值和生产采集/通知验证;SDK受控重连风暴和真实慢消费者背压已补[边界回归](reports/phase-6-reconnect-storm.md)与[慢消费者报告](reports/phase-6-slow-consumer.md),跨节点重连仍待目标拓扑。
|
2. 已实现[基础运行观测](api/observability.md)及[有界业务状态与事件采样](reports/phase-6-business-observability.md):HTTP/WS、数据库连接池、后台批次、固定任务/交付/流程/Inbox状态计数、最近事件类别、终态耗时样本、固定事件阶段耗时、最早年龄、截断/新鲜度指标、提交前 MySQL 事务重试/耗尽计数和可离线验证的告警规则;独立采集监听默认关闭,固定指标目录拒绝动态标签;发布前只读结构核验由[`orbit-admin verify`](api/operations.md)提供,运维人员也可用 `orbit-admin observe snapshot` 获取同一有界单节点快照;后台循环间隔、批量上限、Worker/Event连接上限、Hub领取协程和有界内存排队均支持有界环境配置,并在启动日志和固定指标中记录。跨节点重连风暴、慢消费者、真实锁等待/死锁、磁盘增长已经加入定向回归入口;目标拓扑的全局持久吞吐、锁等待/死锁告警阈值和生产采集/通知验证仍需环境验收。
|
||||||
3. 已补齐受保护管理环境中的固定凭据操作文件、轮换、撤销、提交不明故障切点、幂等回执和脱敏管理审计;轮换保留授权绑定并要求新令牌重新建立会话,旧 Worker 会话被排空且立即过期,撤销允许在停用范围内执行且不改写历史记录。已用一次性 MySQL 定向验证旧令牌失效、新令牌重绑和零外键;仍需多节点安全重绑演练、凭据最小权限配置核对和生产密钥托管/审计演练。
|
3. 已补齐受保护管理环境中的固定凭据操作文件、轮换、撤销、提交不明故障切点、幂等回执和脱敏管理审计;轮换保留授权绑定并要求新令牌重新建立会话,旧 Worker 会话被排空且立即过期,撤销允许在停用范围内执行且不改写历史记录。已用一次性 MySQL 定向验证旧令牌失效、新令牌重绑和零外键;仍需多节点安全重绑演练、凭据最小权限配置核对和生产密钥托管/审计演练。
|
||||||
4. 已补[`orbit-admin retention inventory`](api/retention.md)只读盘点和[阶段报告](reports/phase-6-retention-inventory.md),能区分过期幂等、确定终态、未知效果、未终结流程、已确认/未确认交付、已消费/未消费Inbox、事件和审计规模;它不执行归档或删除。已补[容量模型](19-capacity-model.md),把企业数转换为任务、流程、事件、连接、写放大和保留期输入;仍需评审各类保留周期、归档格式、受保护记录核对,再执行备份恢复、兼容升级和六类容量负载。未确认交付、有效幂等身份、当前/历史执行证据和未终结流程不能被普通清理破坏。生产HA及RPO/RTO继续按实际拓扑验证,不缩减为本机多进程检查。
|
4. 已补[数据库保留策略、预览和只读归档导出](api/retention.md)以及[`orbit-admin retention inventory`](api/retention.md)只读盘点。策略按命名空间存入 MySQL,控制台单管理员可修改,版本、原因和前后快照进入审计;默认关闭归档,源数据不会因配置变化自动删除。固定候选目录保护有效幂等身份、未确认交付、未知效果、未终结流程和未处理 Inbox;导出包带冻结策略版本和 SHA-256;控制台支持连续批次、累计数量、摘要校验和本批重下载,真实 MySQL 回归核对同时间戳分页、保护对象、版本及权限边界。已补[持久文件归档作业](api/retention-jobs.md):自动有限分批、原目录续跑、原子发布、容量限制、摘要链、离线核验及真实强杀恢复;尚未接入周期调度、控制台作业列表或跨节点自动接管。仍需在目标环境评审最终归档介质、备份恢复、删除执行器和保留周期,再执行兼容升级和容量负载。生产HA及RPO/RTO继续按实际拓扑验证,不缩减为本机多进程检查。
|
||||||
5. 已补[`orbit-load`](api/load.md)有界受理负载工具及24次/1000次隔离样本,见[受理报告](reports/phase-6-load.md)。新增独立[`orbit-exec-load`](api/execution-load.md),通过真实Worker、任务结果和尝试历史核对,提供短/混合/中长任务的受理、排队、授权执行及完成耗时分布;具体环境、原始JSON和失败分母见[执行测量报告](reports/phase-6-execution-load.md)。新增[`orbit-workflow-load`](api/workflow-load.md),用本次发布版本的双分支并行流程,逐个核对不同任务、唯一尝试、来源、精确输出与本地调用;24/100实例及24实例截止负例已重新构建回归并绑定源码/二进制哈希,见[流程测量报告](reports/phase-6-workflow-load.md)。新增[`orbit-schedule-load`](api/schedule-load.md),已完成24/128次定时集中触发与24次计划量的截止负例,保存实际分母、唯一任务/尝试、来源与暂停收尾证据,见[定时测量报告](reports/phase-6-schedule-load.md)。Go 1.27.1 模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24个可运行 SDK 示例真实服务串跑和 result-inbox/task-outbox 真实业务集成已复跑通过。已补[长期积压与资源曲线样本](reports/phase-6-backlog.md):32×8 共256个触发点,在1个Worker/容量1和180秒截止下保留173个成功、83个未核验任务,并以独立数据库读取记录等待积压曲线;该单节点样本不替代目标拓扑容量。SDK受控重连风暴及[真实慢消费者背压](reports/phase-6-slow-consumer.md)已补边界回归,积压脚本已加入 CPU/RSS、MySQL 资源、连接池等待、业务快照和事务重试计数采样;下一步补长期存储增长、跨节点重连与目标数据库拓扑容量。
|
5. 已补[`orbit-load`](api/load.md)有界受理负载工具及24次/1000次隔离样本,见[受理报告](reports/phase-6-load.md)。新增独立[`orbit-exec-load`](api/execution-load.md),通过真实Worker、任务结果和尝试历史核对,提供短/混合/中长任务的受理、排队、授权执行及完成耗时分布;具体环境、原始JSON和失败分母见[执行测量报告](reports/phase-6-execution-load.md)。新增[`orbit-workflow-load`](api/workflow-load.md),用本次发布版本的双分支并行流程,逐个核对不同任务、唯一尝试、来源、精确输出与本地调用;24/100实例及24实例截止负例已重新构建回归并绑定源码/二进制哈希,见[流程测量报告](reports/phase-6-workflow-load.md)。新增[`orbit-schedule-load`](api/schedule-load.md),已完成24/128次定时集中触发与24次计划量的截止负例,保存实际分母、唯一任务/尝试、来源与暂停收尾证据,见[定时测量报告](reports/phase-6-schedule-load.md)。Go 1.27.1 模块级门禁、MySQL 存储集成、关键 SDK/Orbit 进程故障组、24个可运行 SDK 示例真实服务串跑和 result-inbox/task-outbox 真实业务集成已复跑通过。已补[长期积压与资源曲线样本](reports/phase-6-backlog.md):32×8 共256个触发点,在1个Worker/容量1和180秒截止下保留173个成功、83个未核验任务,并以独立数据库读取记录等待积压曲线;该单节点样本不替代目标拓扑容量。SDK受控重连风暴及[真实慢消费者背压](reports/phase-6-slow-consumer.md)已补边界回归,积压脚本已加入 CPU/RSS、MySQL 资源、连接池等待、业务快照和事务重试计数采样;已补跨节点重连、慢消费者与有界磁盘增长样本(见 [P1 定向报告](reports/phase-6-stability-p1.md));长期磁盘曲线与目标数据库拓扑容量继续按实际负载验收。
|
||||||
6. 已补[隔离备份恢复演练](api/backup-restore.md):源库和恢复库均为一次性 MySQL 容器,Go 1.27.1 构建的当前二进制在逻辑备份恢复后核对迁移账本、核心记录数量、固定索引和零外键;证据见[恢复报告](reports/phase-6-backup-restore.md)。该演练不替代生产备份介质、复制切换、跨版本恢复或 RPO/RTO 验收。
|
6. 已补[隔离备份恢复演练](api/backup-restore.md):源库和恢复库均为一次性 MySQL 容器,Go 1.27.1 构建的当前二进制在逻辑备份恢复后核对迁移账本、核心记录数量、固定索引和零外键;证据见[恢复报告](reports/phase-6-backup-restore.md)。该演练不替代生产备份介质、复制切换、跨版本恢复或 RPO/RTO 验收。
|
||||||
7. 已补[A18 数据库短暂不可用恢复演练](api/db-recovery.md):Go 1.27.1 构建的当前二进制在数据库停止期间返回 readiness 503,同卷恢复后返回 200,并用原权限范围继续受理任务;证据见[数据库恢复报告](reports/phase-6-db-recovery.md)。该演练不替代主从切换、复制确认、跨故障域或 RPO/RTO 验收。
|
7. 已补[A18 数据库短暂不可用恢复演练](api/db-recovery.md):Go 1.27.1 构建的当前二进制在数据库停止期间返回 readiness 503,同卷恢复后返回 200,并用原权限范围继续受理任务;证据见[数据库恢复报告](reports/phase-6-db-recovery.md)。该演练不替代主从切换、复制确认、跨故障域或 RPO/RTO 验收。
|
||||||
8. 已补[MySQL GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md):隔离源库/副本通过真实 Orbit HTTP 受理任务,源库停止后副本提升、结构核验和原幂等键重放通过;自动故障切换、复制延迟上限、目标拓扑及 RPO/RTO 仍待验收。
|
8. 已补[MySQL GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md):隔离源库/副本通过真实 Orbit HTTP 受理任务,源库停止后副本提升、结构核验和原幂等键重放通过;自动故障切换、复制延迟上限、目标拓扑及 RPO/RTO 仍待验收。
|
||||||
@@ -213,6 +235,8 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
|
|||||||
- [P2 SDK 协议阶段记录](reports/phase-2-sdk-protocol.md)
|
- [P2 SDK 协议阶段记录](reports/phase-2-sdk-protocol.md)
|
||||||
- [P2 自动 Worker 运行器记录](reports/phase-2-worker-runtime.md)
|
- [P2 自动 Worker 运行器记录](reports/phase-2-worker-runtime.md)
|
||||||
- [P2 执行历史与大载荷记录](reports/phase-2-execution-history.md)
|
- [P2 执行历史与大载荷记录](reports/phase-2-execution-history.md)
|
||||||
|
- [P1 稳定性与归档定向回归](reports/phase-6-stability-p1.md)
|
||||||
|
- [持久文件归档作业回归](reports/phase-6-retention-jobs.md)
|
||||||
|
|
||||||
### 状态事件时间线子项
|
### 状态事件时间线子项
|
||||||
|
|
||||||
@@ -220,4 +244,25 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
|
|||||||
|
|
||||||
### P4/P5 阶段结论与下一批
|
### P4/P5 阶段结论与下一批
|
||||||
|
|
||||||
[逐项证据与生产边界](reports/phase-4-5.md)已落表,P4/P5平台功能阶段验收通过。W06/W09/W14/W15真实竞争和全停、A04未送达、W01/W13提交断线及W16旧结果重放竞争均有对应回归,不重复列为待开发。下一批首先补运行指标与告警依据,再按上方P6顺序推进凭据轮换、保留/归档、容量、备份恢复与生产数据库故障验证;P7负责剩余场景和版本发布。完整首版目标保持进行中。
|
[逐项证据与生产边界](reports/phase-4-5.md)已落表,P4/P5平台功能阶段验收通过。W06/W09/W14/W15真实竞争和全停、A04未送达、W01/W13提交断线及W16旧结果重放竞争均有对应回归,不重复列为待开发。运行指标、凭据轮换、保留策略及只读归档、隔离容量样本和备份恢复均已按上方 P6 留有验证记录;后续重点是归档介质与删除执行器、长期磁盘增长、目标生产拓扑的容量/数据库故障及 RPO/RTO 验收。P7 负责剩余场景和版本发布。完整首版目标保持进行中。
|
||||||
|
|
||||||
|
### 周期归档与控制台作业(本批)
|
||||||
|
|
||||||
|
- 已实现:000031 无外键计划/作业模型及中文注释;按类别配置周期与预算;数据库时间合并错过周期;固定版本/卷身份;有界领取、过期接管与迟到隔离;连续文件链和数据库前缀核验;单作业及全卷预算;单管理员页面、分页、失败重试/关闭、幂等与审计。
|
||||||
|
- 已验证:真实 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或源数据删除。
|
||||||
|
|
||||||
|
### main、正式版本与统一示例(本批)
|
||||||
|
|
||||||
|
- 根 examples 统一 27 个可运行场景,各目录 README 说明用途、权限、运行和结果;首页改为完整功能与接入入口。
|
||||||
|
- 开发工作区、构建、MySQL/控制台/历史 SDK 回归同步迁移路径;SDK 保持独立模块。
|
||||||
|
- 使用 main 与 v0.1.1 / sdk/go/v0.1.1 / adapters/outbox/mysql/v0.1.1 交付,旧标签不变;业务固定版本,无需开发分支或提交号。
|
||||||
|
- 标准 7 模块回归、SDK/协议/文档门禁、真实 MySQL 全量、控制台端到端及历史任务 API 双向 smoke 均通过;远端安装结果见[发布记录](reports/v0.1.1-release.md)。
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ docker image inspect registry.example/orbit:dev --format '{{.Id}}'
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build --platform linux/amd64 \
|
docker build --platform linux/amd64 \
|
||||||
--build-arg VERSION=1.0.0 \
|
--build-arg VERSION=v0.1.1 \
|
||||||
--build-arg REVISION="$(git rev-parse HEAD)" \
|
--build-arg REVISION="$(git rev-parse HEAD)" \
|
||||||
--build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
--build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
-t registry.example/orbit:1.0.0 .
|
-t registry.example/orbit:v0.1.1 .
|
||||||
```
|
```
|
||||||
|
|
||||||
镜像同时写入 OCI `org.opencontainers.image.version`、`org.opencontainers.image.revision` 和 `org.opencontainers.image.created` 标签;可以先核对标签,再在节点启动后通过 `GET /version` 核对进程实际使用的值。`registry.example` 只是占位符,不是平台依赖。
|
镜像同时写入 OCI `org.opencontainers.image.version`、`org.opencontainers.image.revision` 和 `org.opencontainers.image.created` 标签;可以先核对标签,再在节点启动后通过 `GET /version` 核对进程实际使用的值。`registry.example` 只是占位符,不是平台依赖。
|
||||||
@@ -33,7 +33,7 @@ docker build --platform linux/amd64 \
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run --rm --entrypoint /usr/local/bin/orbit-admin \
|
docker run --rm --entrypoint /usr/local/bin/orbit-admin \
|
||||||
registry.example/orbit:1.0.0 release manifest > orbit-release-manifest.json
|
registry.example/orbit:v0.1.1 release manifest > orbit-release-manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
清单把当前构建身份、协议版本和内置迁移校验和固定下来;部署前再用 `orbit-admin verify` 对目标数据库逐项核对。两者不一致时停止发布,不通过人工修改清单来“对齐”。
|
清单把当前构建身份、协议版本和内置迁移校验和固定下来;部署前再用 `orbit-admin verify` 对目标数据库逐项核对。两者不一致时停止发布,不通过人工修改清单来“对齐”。
|
||||||
@@ -60,11 +60,12 @@ docker run --rm \
|
|||||||
--env ORBIT_MYSQL_DSN \
|
--env ORBIT_MYSQL_DSN \
|
||||||
--entrypoint /usr/local/bin/orbit-admin \
|
--entrypoint /usr/local/bin/orbit-admin \
|
||||||
-v "$PWD/orbit-release-manifest.json:/run/orbit-release-manifest.json:ro" \
|
-v "$PWD/orbit-release-manifest.json:/run/orbit-release-manifest.json:ro" \
|
||||||
registry.example/orbit:1.0.0 verify --manifest /run/orbit-release-manifest.json
|
registry.example/orbit:v0.1.1 verify --manifest /run/orbit-release-manifest.json
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--env ORBIT_MYSQL_DSN \
|
--env ORBIT_MYSQL_DSN \
|
||||||
--env ORBIT_HTTP_ADDR=0.0.0.0:8080 \
|
--env ORBIT_HTTP_ADDR=0.0.0.0:8080 \
|
||||||
|
--env ORBIT_CONSOLE_QUEUE_ID=queue_xxx \
|
||||||
--env ORBIT_RUNTIME_BATCH_LIMIT=100 \
|
--env ORBIT_RUNTIME_BATCH_LIMIT=100 \
|
||||||
--env ORBIT_WORKFLOW_TICK_INTERVAL=1s \
|
--env ORBIT_WORKFLOW_TICK_INTERVAL=1s \
|
||||||
--env ORBIT_SCHEDULE_TICK_INTERVAL=1s \
|
--env ORBIT_SCHEDULE_TICK_INTERVAL=1s \
|
||||||
@@ -79,6 +80,8 @@ docker run --rm \
|
|||||||
registry.example/orbit:dev
|
registry.example/orbit:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ORBIT_CONSOLE_QUEUE_ID` 是控制台的可选默认队列范围,只用于打开 `/console/` 时预填队列 ID,不是认证凭据。控制台登录使用数据库中的管理员账号,会话建立后服务端按账号绑定的业务凭据读取统计和任务;浏览器不需要填写 API 地址或上传凭据 JSON。多队列部署可以省略此变量,管理员在控制台内选择有权限的队列范围。首次管理员通过 `orbit-admin bootstrap` 创建,后续账号使用 `orbit-admin console-user create` 创建。
|
||||||
|
|
||||||
生产编排应先完成迁移和 `verify`,再启动新节点。服务收到 `SIGTERM` 后停止接收新连接并在有限窗口内关闭 HTTP、WebSocket 和后台循环;编排器的终止宽限期必须覆盖该窗口。启动后才能把节点加入流量池。
|
生产编排应先完成迁移和 `verify`,再启动新节点。服务收到 `SIGTERM` 后停止接收新连接并在有限窗口内关闭 HTTP、WebSocket 和后台循环;编排器的终止宽限期必须覆盖该窗口。启动后才能把节点加入流量池。
|
||||||
|
|
||||||
## 3. 探针和监听边界
|
## 3. 探针和监听边界
|
||||||
|
|||||||
154
docs/22-compose-deployment.md
Normal file
154
docs/22-compose-deployment.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Orbit Compose 部署基线
|
||||||
|
|
||||||
|
本文给出单节点部署的可重复入口,适合开发、验收和小规模生产前置环境。它把 MySQL、迁移、schema 核验和 Orbit 服务串成一条有明确失败边界的启动链路;生产多节点、数据库高可用、备份恢复和容量门禁仍按[生产验收](05-production-acceptance.md)执行。
|
||||||
|
|
||||||
|
前置条件:Docker Engine、Docker Compose v2.20 或更高版本;如果使用仓库里的 Nginx 示例,需要 Nginx 1.25.1 或更高版本;正式环境还应具备镜像仓库、Secret 管理器和 HTTPS 反向代理。
|
||||||
|
|
||||||
|
## 1. 启动契约
|
||||||
|
|
||||||
|
`deploy/docker-compose.yml` 使用同一个 Orbit 镜像启动 Orbit 相关服务,并使用一个短生命周期的 MySQL 客户端服务收紧运行账号权限:
|
||||||
|
|
||||||
|
1. `mysql` 先通过 MySQL 8.4 健康检查。
|
||||||
|
2. `migrate` 使用迁移账号执行 `orbit-migrate up`。
|
||||||
|
3. `grant` 撤销 MySQL 初始化时给运行账号的全库权限,只保留 Orbit 运行所需的 `SELECT`、`INSERT`、`UPDATE`、`DELETE`。
|
||||||
|
4. `verify` 使用收紧后的运行账号执行 `orbit-admin verify`。
|
||||||
|
5. `orbit` 只有在迁移、权限收紧和核验都成功后才启动,运行账号不承担迁移职责。
|
||||||
|
|
||||||
|
迁移或核验失败时,Compose 的 `service_completed_successfully` 依赖会阻止 Orbit 进入流量。修复数据库或发布物后,用同一项目重新执行 `docker compose up -d`;不要通过删除迁移记录或手工改表来绕过核验。
|
||||||
|
|
||||||
|
Compose 文件要求显式提供数据库密码,仓库没有默认密码,也不读取隐式凭据。密码应来自部署平台的 Secret 注入,不应提交到 Git、镜像或日志。
|
||||||
|
|
||||||
|
## 2. 首次启动
|
||||||
|
|
||||||
|
先构建不可变镜像。正式发布时应把 `ORBIT_IMAGE` 换成带版本和摘要的镜像地址:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build \
|
||||||
|
--build-arg VERSION=v0.1.1 \
|
||||||
|
--build-arg REVISION="$(git rev-parse HEAD)" \
|
||||||
|
--build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
-t orbit:v0.1.1 .
|
||||||
|
```
|
||||||
|
|
||||||
|
在当前 shell 或 Secret 管理器中注入配置。以下值仅是变量示例,不是生产密码:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export ORBIT_IMAGE=orbit:v0.1.1
|
||||||
|
export ORBIT_DB_NAME=orbit
|
||||||
|
export ORBIT_DB_USER=orbit_runtime
|
||||||
|
export ORBIT_DB_PASSWORD='从秘密管理器读取'
|
||||||
|
export ORBIT_DB_ROOT_PASSWORD='从秘密管理器读取'
|
||||||
|
export ORBIT_HTTP_BIND=127.0.0.1
|
||||||
|
export ORBIT_HTTP_PORT=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
`ORBIT_DB_NAME` 和 `ORBIT_DB_USER` 仅使用字母、数字和下划线;权限收紧服务会拒绝其他字符。数据库密码由 Secret 管理器注入,建议使用字母、数字和短横线组成的随机值,避免 DSN 转义差异。
|
||||||
|
|
||||||
|
首次启动先只拉起 MySQL、迁移、权限收紧和核验:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f deploy/docker-compose.yml up -d mysql verify
|
||||||
|
docker compose -f deploy/docker-compose.yml ps
|
||||||
|
docker compose -f deploy/docker-compose.yml logs --no-log-prefix migrate grant verify
|
||||||
|
```
|
||||||
|
|
||||||
|
`verify` 成功后,使用同一镜像和数据库连接创建初始业务资源与控制台管理员。密码通过标准输入传入,不能写在命令行参数中:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
credential_dir="$(mktemp -d)"
|
||||||
|
umask 077
|
||||||
|
printf '%s\n' "$ORBIT_CONSOLE_PASSWORD" | \
|
||||||
|
docker compose -f deploy/docker-compose.yml run --rm -T --no-deps \
|
||||||
|
--entrypoint /usr/local/bin/orbit-admin \
|
||||||
|
-e ORBIT_MYSQL_DSN="root:${ORBIT_DB_ROOT_PASSWORD}@tcp(mysql:3306)/${ORBIT_DB_NAME:-orbit}?parseTime=true&multiStatements=true" \
|
||||||
|
-v "$credential_dir:/run/orbit" migrate bootstrap \
|
||||||
|
--application platform \
|
||||||
|
--namespace default \
|
||||||
|
--environment production \
|
||||||
|
--queue default \
|
||||||
|
--worker-access \
|
||||||
|
--schedule-access \
|
||||||
|
--console-username "$ORBIT_CONSOLE_USERNAME" \
|
||||||
|
--console-display-name 'Orbit 管理员' \
|
||||||
|
--console-password-stdin \
|
||||||
|
--output /run/orbit/credentials.json
|
||||||
|
```
|
||||||
|
|
||||||
|
将 `credentials.json` 按 Secret 文件处理,仅用于保存业务接入凭据;控制台登录使用上面创建的用户名和密码。完成安全保存后再启动服务:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export ORBIT_CONSOLE_QUEUE_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["queue_id"])' "$credential_dir/credentials.json")"
|
||||||
|
docker compose -f deploy/docker-compose.yml up -d orbit
|
||||||
|
curl --fail http://127.0.0.1:"${ORBIT_HTTP_PORT:-8080}"/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
如果只需业务 SDK 接入,可以省略控制台管理员;如果要创建多个管理员,使用 `orbit-admin console-user create`,每个账号单独审计和轮换,不共享密码。
|
||||||
|
|
||||||
|
## 3. 访问和域名
|
||||||
|
|
||||||
|
Orbit Console 使用当前页面的同源地址调用服务端,浏览器不需要再填写 API URL,也不需要上传凭据 JSON 才能查看控制台。直接访问 `http://主机:端口/console/`,未登录会跳转到 `/console/login`。
|
||||||
|
|
||||||
|
接入域名时,参考 [`deploy/nginx/orbit.conf.example`](../deploy/nginx/orbit.conf.example),将 HTTPS 反向代理转发到 Orbit 的 `8080` 端口,并保留 WebSocket Upgrade/Connection 头;浏览器访问的域名就是 Console 自动使用的 API 地址。`ORBIT_HTTP_BIND` 控制主机监听地址,`ORBIT_HTTP_PORT` 控制主机映射端口;两者都不是业务域名配置。生产建议保持 `ORBIT_HTTP_BIND=127.0.0.1`,只允许同机 Nginx 访问。TLS 证书、域名和安全头由反向代理或入口网关管理。
|
||||||
|
|
||||||
|
修改 Nginx 配置后先执行 `nginx -t`,再平滑加载配置;不要在未验证证书、HTTP 到 HTTPS 跳转和 WSS 升级前切换公网流量。Nginx 不应代理 Orbit 的独立 metrics 监听,metrics 只在内部采集网络开放。
|
||||||
|
|
||||||
|
## 4. 日常操作与失败处理
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 查看迁移、核验和服务状态
|
||||||
|
docker compose -f deploy/docker-compose.yml ps
|
||||||
|
|
||||||
|
# 读取结构化服务日志
|
||||||
|
docker compose -f deploy/docker-compose.yml logs --tail=200 orbit
|
||||||
|
|
||||||
|
# 发布新镜像:先完成迁移和核验,再启动新服务
|
||||||
|
docker compose -f deploy/docker-compose.yml up -d verify
|
||||||
|
docker compose -f deploy/docker-compose.yml up -d orbit
|
||||||
|
|
||||||
|
# 停止服务但保留数据库卷
|
||||||
|
docker compose -f deploy/docker-compose.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose 没有为 scratch 镜像伪造容器内 HTTP healthcheck;编排器或入口网关应调用 `/health/live`、`/health/ready` 和 `/version`。`ready` 返回 `503` 时摘除节点,保留原幂等键并等待数据库恢复。只有明确的废弃环境才使用 `docker compose down -v`,生产数据卷不得随服务停止删除。
|
||||||
|
|
||||||
|
## 5. 两节点覆盖与滚动替换
|
||||||
|
|
||||||
|
需要在同一台主机验证多节点接管时,叠加 `deploy/docker-compose.ha.yml`。两个 Orbit 节点共享 MySQL、迁移账本、租约和流程检查点;每个进程仍生成独立节点标识,停止一个节点不会把另一个节点的持久化执行权一并停止:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export ORBIT_HTTP_BIND=127.0.0.1
|
||||||
|
export ORBIT_HTTP_PORT_A=8081
|
||||||
|
export ORBIT_HTTP_PORT_B=8082
|
||||||
|
|
||||||
|
docker compose \
|
||||||
|
-f deploy/docker-compose.yml \
|
||||||
|
-f deploy/docker-compose.ha.yml \
|
||||||
|
up -d mysql verify orbit-a orbit-b
|
||||||
|
|
||||||
|
curl --fail http://127.0.0.1:8081/health/ready
|
||||||
|
curl --fail http://127.0.0.1:8082/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
入口层把 Nginx `upstream orbit_backend` 改为两个后端:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
upstream orbit_backend {
|
||||||
|
server 127.0.0.1:8081;
|
||||||
|
server 127.0.0.1:8082;
|
||||||
|
keepalive 64;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
滚动替换时先让入口健康检查摘除一个节点,再停止并更新它;确认另一节点 `ready` 后再恢复流量,最后启动更新后的节点并等待两个后端都通过健康检查:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.ha.yml stop orbit-a
|
||||||
|
# 更新镜像标签后重新拉起 orbit-a;迁移和 verify 仍必须先成功
|
||||||
|
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.ha.yml up -d orbit-a
|
||||||
|
```
|
||||||
|
|
||||||
|
这个覆盖配置用于单机和隔离验收;生产环境应把两个 Orbit 节点放在不同故障域,前置负载均衡器负责健康摘除,MySQL 由独立数据库高可用方案提供,不把单机 Compose 多节点样本当成生产 HA 结论。
|
||||||
|
|
||||||
|
## 6. 当前边界
|
||||||
|
|
||||||
|
该文件是可重复的单节点启动基线,不代表自动数据库故障切换、跨故障域多副本、备份恢复或容量目标已经验收。生产上线还需要真实 MySQL 拓扑、滚动替换、凭据轮换、备份恢复、压测和 RPO/RTO 证据;对应门槛以[交付核对清单](release-checklist.md)为准。
|
||||||
@@ -19,8 +19,16 @@
|
|||||||
| [08 持续开发计划](08-development-plan.md) | 开发、测试回归、阶段验收与最终交付目标 |
|
| [08 持续开发计划](08-development-plan.md) | 开发、测试回归、阶段验收与最终交付目标 |
|
||||||
| [19 容量模型](19-capacity-model.md) | 企业规模到任务、流程、数据库与连接负载的推导及生产验收方法 |
|
| [19 容量模型](19-capacity-model.md) | 企业规模到任务、流程、数据库与连接负载的推导及生产验收方法 |
|
||||||
| [P7 协议兼容矩阵](reports/phase-7-compatibility.md) | 当前协议门禁、历史提交双向任务 API 演练与正式标签矩阵边界 |
|
| [P7 协议兼容矩阵](reports/phase-7-compatibility.md) | 当前协议门禁、历史提交双向任务 API 演练与正式标签矩阵边界 |
|
||||||
|
| [P7 Console 真实链路回归](reports/phase-7-console-e2e.md) | 真实 MySQL、Worker、流程、结果订阅与控制台读取闭环 |
|
||||||
|
| [首次部署与控制台任务闭环](reports/phase-8-console-e2e.md) | 从空库初始化管理员、Docker 启动、控制台登录、WS Worker、重试和定时任务 |
|
||||||
|
| [Compose 部署基线验证](reports/phase-9-compose-deployment.md) | MySQL、迁移、核验、Orbit 启动顺序及失败门禁 |
|
||||||
|
| [HTTPS/WSS 入口基线验证](reports/phase-10-ingress.md) | 本机端口边界、Nginx TLS、WebSocket 升级和健康探针配置 |
|
||||||
| [20 运行与发布手册](20-operations-runbook.md) | 启动、发布、滚动停止、数据库恢复、备份和事故处理 |
|
| [20 运行与发布手册](20-operations-runbook.md) | 启动、发布、滚动停止、数据库恢复、备份和事故处理 |
|
||||||
| [21 容器发布说明](21-container-deployment.md) | 最小镜像、迁移/核验/启动顺序、探针和发布边界 |
|
| [21 容器发布说明](21-container-deployment.md) | 最小镜像、迁移/核验/启动顺序、探针和发布边界 |
|
||||||
|
| [22 Compose 部署基线](22-compose-deployment.md) | MySQL、迁移、核验、Orbit 服务和首次管理员初始化的可重复启动链路 |
|
||||||
|
| [Nginx HTTPS/WSS 入口示例](../deploy/nginx/orbit.conf.example) | 域名、TLS、WebSocket 升级、健康探针和控制台登录限速 |
|
||||||
|
| [Compose 两节点覆盖](../deploy/docker-compose.ha.yml) | 共享 MySQL 状态、双节点就绪和滚动替换验收入口 |
|
||||||
|
| [两节点 Compose 覆盖验证](reports/phase-11-two-node-compose.md) | 双节点启动、单节点停止后的对等节点可用和重新加入 |
|
||||||
| [09 可靠结果订阅](09-result-subscription-core.md) | 提交水位、冻结结果与可靠交付 |
|
| [09 可靠结果订阅](09-result-subscription-core.md) | 提交水位、冻结结果与可靠交付 |
|
||||||
| [10 Weave决策与交付](10-workflow-engine-core.md) | 有限步骤决策和内部结果Inbox |
|
| [10 Weave决策与交付](10-workflow-engine-core.md) | 有限步骤决策和内部结果Inbox |
|
||||||
| [11 Weave实例受理](11-workflow-admission.md) | 固定版本、初始图和首批任务事务 |
|
| [11 Weave实例受理](11-workflow-admission.md) | 固定版本、初始图和首批任务事务 |
|
||||||
@@ -30,6 +38,9 @@
|
|||||||
| [流程状态时间线](api/workflow-events.md) | 原始流程/步骤/信号状态事件、分页和历史详情 |
|
| [流程状态时间线](api/workflow-events.md) | 原始流程/步骤/信号状态事件、分页和历史详情 |
|
||||||
| [接口手册](api/README.md) | 实际接口状态、调用说明与后续接口规划 |
|
| [接口手册](api/README.md) | 实际接口状态、调用说明与后续接口规划 |
|
||||||
| [SDK 与场景](sdk/README.md) | 可运行示例、接入方法及分阶段 SDK 交付 |
|
| [SDK 与场景](sdk/README.md) | 可运行示例、接入方法及分阶段 SDK 交付 |
|
||||||
|
| [Orbit Console](console.md) | 内置控制台、本地打开方式与权限边界 |
|
||||||
|
| [保留策略](api/retention.md) | 管理端窗口配置、候选保护和只读快照 |
|
||||||
|
| [文件归档作业](api/retention-jobs.md) | 自动有限分批、断点续跑、容量限制和离线核验 |
|
||||||
|
|
||||||
## 已确认的决策
|
## 已确认的决策
|
||||||
|
|
||||||
@@ -97,3 +108,7 @@ Orbit 不内置特定行业规则;Pulse 管理执行,Weave 管理步骤依
|
|||||||
持续开发目标已建立。当前实现和验收进度以 [开发计划](08-development-plan.md)、[P0 报告](reports/phase-0.md)、[P1 报告](reports/phase-1.md) 与 [P2 阶段验收](reports/phase-2.md) 、[P3 安全重试报告](reports/phase-3-safe-retry.md) 、[P3 计划调度报告](reports/phase-3-schedules.md) 和 [P3 完整范围验收](reports/phase-3.md) 为准;早期设计稿中的“未实现”描述保留其编写阶段语境。
|
持续开发目标已建立。当前实现和验收进度以 [开发计划](08-development-plan.md)、[P0 报告](reports/phase-0.md)、[P1 报告](reports/phase-1.md) 与 [P2 阶段验收](reports/phase-2.md) 、[P3 安全重试报告](reports/phase-3-safe-retry.md) 、[P3 计划调度报告](reports/phase-3-schedules.md) 和 [P3 完整范围验收](reports/phase-3.md) 为准;早期设计稿中的“未实现”描述保留其编写阶段语境。
|
||||||
|
|
||||||
[实例查询接口与SDK](api/workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
|
[实例查询接口与SDK](api/workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
|
||||||
|
|
||||||
|
- [周期归档与作业管理](api/archive-scheduler.md)
|
||||||
|
|
||||||
|
- [周期归档与作业管理验证记录](reports/phase-6-archive-scheduler.md)
|
||||||
|
|||||||
@@ -64,3 +64,5 @@ P1 业务接口已遵守下述约定,后续接口继续沿用:
|
|||||||
实现时必须补齐:用途与权限、HTTP 方法和路径、请求头、字段类型/必填/默认值/限制、完整请求与响应、错误码、幂等与并发规则、超时/重试/取消语义、SDK 对应方法、可运行示例、兼容版本和已覆盖的回归场景。
|
实现时必须补齐:用途与权限、HTTP 方法和路径、请求头、字段类型/必填/默认值/限制、完整请求与响应、错误码、幂等与并发规则、超时/重试/取消语义、SDK 对应方法、可运行示例、兼容版本和已覆盖的回归场景。
|
||||||
|
|
||||||
[流程实例筛选分页与权限](workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
|
[流程实例筛选分页与权限](workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
|
||||||
|
|
||||||
|
- [周期归档与作业管理](archive-scheduler.md)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ curl -X POST "$ORBIT_URL/v1/application/resources:configure" \
|
|||||||
|
|
||||||
## 6. SDK 与可运行场景
|
## 6. SDK 与可运行场景
|
||||||
|
|
||||||
SDK 方法:`GetApplicationResources(ctx)` 和 `ConfigureApplicationResources(ctx, key, request)`。完整双命名空间场景源码:[application-resources](../../sdk/go/examples/application-resources/main.go)。
|
SDK 方法:`GetApplicationResources(ctx)` 和 `ConfigureApplicationResources(ctx, key, request)`。完整双命名空间场景源码:[application-resources](../../examples/application-resources/main.go)。
|
||||||
|
|
||||||
在仓库根目录,先用已迁移数据库启动服务,并初始化一个独立、空闲的示例应用:
|
在仓库根目录,先用已迁移数据库启动服务,并初始化一个独立、空闲的示例应用:
|
||||||
|
|
||||||
@@ -135,11 +135,11 @@ ORBIT_EXAMPLE_APP_ID=$(sed -n 's/.*"application_id":"\([^"]*\)".*/\1/p' .local/a
|
|||||||
go run ./cmd/orbit-admin bootstrap \
|
go run ./cmd/orbit-admin bootstrap \
|
||||||
--application-id "$ORBIT_EXAMPLE_APP_ID" --namespace second --environment development \
|
--application-id "$ORBIT_EXAMPLE_APP_ID" --namespace second --environment development \
|
||||||
--queue second --resource-access --output .local/app-second.credentials.json
|
--queue second --resource-access --output .local/app-second.credentials.json
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/app-first.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/app-first.credentials.json \
|
||||||
ORBIT_SECOND_CREDENTIALS_FILE=../../.local/app-second.credentials.json \
|
ORBIT_SECOND_CREDENTIALS_FILE=../.local/app-second.credentials.json \
|
||||||
go run ./examples/application-resources
|
go run ./application-resources
|
||||||
```
|
```
|
||||||
|
|
||||||
预期:
|
预期:
|
||||||
|
|||||||
146
docs/api/archive-scheduler.md
Normal file
146
docs/api/archive-scheduler.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# 周期归档与作业管理
|
||||||
|
|
||||||
|
Orbit 控制台的「归档作业」页面提供周期配置、作业列表、失败重试及关闭。沿用单管理员账号密码登录,不引入角色体系。业务 SDK 不需要也不能领取归档作业。
|
||||||
|
|
||||||
|
## 1. 部署执行器
|
||||||
|
|
||||||
|
需要 Schema **31**。停止旧节点后执行 `orbit-migrate up`,再启动新版本节点。000031 只增加 `orbit_archive_plans`、`orbit_archive_jobs`,不包含外键或源数据清理。
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
# 必须挂载持久卷,并让 Orbit 运行用户拥有目录,权限 0700。
|
||||||
|
ORBIT_ARCHIVE_ROOT=/var/lib/orbit/archive
|
||||||
|
# 初次部署生成并保存一个固定的 32 位小写十六进制 ID,例如 openssl rand -hex 16。
|
||||||
|
ORBIT_ARCHIVE_STORAGE_ID=11111111111111111111111111111111
|
||||||
|
# 全卷逻辑文件字节预算,默认 10 GiB;范围 1 MiB–1 TiB。
|
||||||
|
ORBIT_ARCHIVE_MAX_BYTES=10737418240
|
||||||
|
# 默认 10s,范围 1s–1m。
|
||||||
|
ORBIT_ARCHIVE_TICK_INTERVAL=10s
|
||||||
|
```
|
||||||
|
|
||||||
|
`ROOT` 和 `STORAGE_ID` 均为空时关闭此节点后台执行器,控制台仍可查询历史。两者必须一起设置。路径只来自部署配置,页面不能指定任意服务器目录。镜像用户为 `65532:65532`,目录须可由该用户访问。
|
||||||
|
|
||||||
|
运行示意(`orbit.env` 同时包含已有数据库等配置,不要提交到 Git):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ./archive-data
|
||||||
|
chmod 700 ./archive-data
|
||||||
|
# Linux 主机上按镜像实际 UID/GID 配置归档目录归属。
|
||||||
|
sudo chown 65532:65532 ./archive-data
|
||||||
|
docker run --name orbit --env-file orbit.env \
|
||||||
|
--mount type=bind,src="$(pwd)/archive-data",dst=/var/lib/orbit/archive \
|
||||||
|
-p 127.0.0.1:8080:8080 orbit:local
|
||||||
|
```
|
||||||
|
|
||||||
|
同一 `STORAGE_ID` 的所有执行节点**必须挂载同一物理卷**,支持 POSIX 文件锁、原子硬链接和目录 fsync;同名路径、复制目录或相同 ID 不等于共享存储。卷根目录持久化 `.storage-id` 身份标记,恢复已有作业时要求原清单存在。独立本地盘使用不同卷 ID;其归档作业不能由别的独立盘接管。没有验证任意 NFS/对象存储语义,不据此宣称跨故障域归档高可用。
|
||||||
|
|
||||||
|
同卷执行串行,后台每轮最多调度 16 个到期类别、执行一个文件批次,工作截止 45 秒,数据库租约 5 分钟。正常继续在下一轮领取;进程异常退出且未确认时,等待租约到期后恢复。关机取消不写成文件损坏。
|
||||||
|
|
||||||
|
## 2. 使用顺序
|
||||||
|
|
||||||
|
1. 登录控制台,在「保留策略」确认各类保留天数,开启只读归档并保存。
|
||||||
|
2. 打开「归档作业」,选择类别,配置间隔、批大小、单作业容量和变更原因,开启周期并保存。
|
||||||
|
3. 首次启用立即到期,后台生成固定身份的作业;页面每 10 秒刷新,显示批次、根记录数、文件字节和链尾 SHA-256。
|
||||||
|
4. 失败时查看分类,修复原因后填写处理原因,从原作业重试;不能恢复的旧版本作业可以关闭,原文件与审计保留。
|
||||||
|
|
||||||
|
任务、流程、事件、审计、交付、Inbox、幂等记录各有独立计划。间隔范围 **1–720 小时**,HTTP 支持 3600–2592000 整数秒;每批 **1–100** 条根记录,单作业 **1 MiB–1 GiB**。关联历史行不计入根记录数,但全部计入文件容量。
|
||||||
|
|
||||||
|
同一类别最多一个未结清作业,失败也占位,避免周期不断制造重复失败。错过多个周期合并触发一次,`next_run_at` 按本次数据库时间加周期计算,不补齐每个错过的时间槽。关闭计划仅停止新作业,已有作业继续结清。
|
||||||
|
|
||||||
|
每次周期是**新的有限全量候选快照**:会再次读取仍满足保留窗口的源记录,不是跨作业增量清理。每个作业内的分页截止、游标、类别、策略版本及批大小固定。文件落盘后才确认数据库进度;已落盘而未确认的文件可在恢复时重新核验并收敛。源数据库内容不删除。
|
||||||
|
|
||||||
|
保留策略修改后,计划仍绑定旧版本,因此不会再生成新作业,须重新保存计划确认新版本。正在运行的旧作业在下一次导出时以 `POLICY_CHANGED` 停止;若已完整落盘,则可核验并确认已完成的原快照。旧作业重试不能更换策略版本或重置截止。
|
||||||
|
|
||||||
|
## 3. HTTP 接口
|
||||||
|
|
||||||
|
以下接口只接受控制台 HttpOnly 会话 Cookie,普通 Bearer 凭据不可访问。命名空间取自会话,写请求拒绝跨来源浏览器调用。保存与人工操作必须提供一个 `Idempotency-Key`;网络或服务器错误后保持原路径、原请求体和原键重试。明确的 4xx 拒绝可核对并修正后使用新键。
|
||||||
|
|
||||||
|
### 查询周期配置
|
||||||
|
|
||||||
|
`GET /console/archive/plans`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"data":{"executor_configured":true,"storage_id":"11111111111111111111111111111111","items":[{"namespace_id":"22222222222222222222222222222222","kind":"tasks","revision":"1","enabled":true,"policy_revision":"3","storage_id":"11111111111111111111111111111111","interval_seconds":86400,"batch_size":20,"max_bytes":268435456,"next_run_at":"2026-09-24T02:00:00Z","active_job_id":null,"updated_at":"2026-09-23T02:00:00Z"}]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
实际返回全部七类。未保存的类别 `revision="0"`、`enabled=false`,没有卷绑定,时间为空。`executor_configured` 只表示当前 HTTP 节点已配置执行器,不表示实时健康探针或整个集群状态。
|
||||||
|
|
||||||
|
### 保存周期配置
|
||||||
|
|
||||||
|
`PUT /console/archive/plans/{kind}`
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
Idempotency-Key: archive-plan-change-001
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expected_revision":"0",
|
||||||
|
"policy_revision":"3",
|
||||||
|
"enabled":true,
|
||||||
|
"interval_seconds":86400,
|
||||||
|
"batch_size":20,
|
||||||
|
"max_bytes":268435456,
|
||||||
|
"reason":"每日归档已超过保留窗口的任务历史"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
全部字段必填,原因是 1–1024 个 UTF-8 字节的非空单行。响应 `data` 为保存后的计划。计划版本或保留策略版本不符返回 409;开启时保留策略必须已启用,当前节点必须配置卷。关闭已有计划时允许当前节点未配置执行器。存在未结清作业时不允许更换卷绑定。
|
||||||
|
|
||||||
|
### 查询作业列表
|
||||||
|
|
||||||
|
`GET /console/archive/jobs`,下一页 `GET /console/archive/jobs?before={next_before}`。
|
||||||
|
|
||||||
|
每页最多 20 条,按创建时间和 ID 倒序;位置必须是当前命名空间内的作业 ID。不是跨页冻结快照。返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"data":{"items":[{"id":"33333333333333333333333333333333","namespace_id":"22222222222222222222222222222222","kind":"tasks","plan_revision":"1","policy_revision":"3","storage_id":"11111111111111111111111111111111","batch_size":20,"max_bytes":268435456,"state":"SUCCEEDED","revision":"5","attempts":2,"batches":2,"roots":32,"bytes":86124,"last_sha256":"64位小写十六进制摘要","error_code":"","lease_until":null,"created_at":"2026-09-23T02:00:00Z","updated_at":"2026-09-23T02:00:20Z"}],"next_before":""}}
|
||||||
|
```
|
||||||
|
|
||||||
|
`attempts` 是批次领取次数,包含分批继续和崩溃重领,不是业务任务重试次数。`batches/roots/bytes/last_sha256` 只展示已经入库确认的进度,落盘后确认失败时页面可能暂时落后。列表不返回文件正文、分页游标或内部租约代次。
|
||||||
|
|
||||||
|
| 状态 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| PENDING | 等待首批或下一批执行 |
|
||||||
|
| RUNNING | 已领取,租约内执行;到期可接管 |
|
||||||
|
| SUCCEEDED | 全部批次落盘、校验并确认完成 |
|
||||||
|
| FAILED | 等待管理员核查;阻止本类别创建后续作业 |
|
||||||
|
| ABANDONED | 管理员关闭失败作业,文件、数据库历史与审计保留 |
|
||||||
|
|
||||||
|
### 重试或关闭失败作业
|
||||||
|
|
||||||
|
`POST /console/archive/jobs/{id}/actions`
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
Idempotency-Key: archive-job-retry-001
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"expected_revision":"8","action":"retry","max_bytes":536870912,"reason":"确认原归档卷完整,并将单作业预算提高到512MiB"}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应 `data` 为操作后的作业。仅 FAILED 可操作;重试保持原 ID、目录、范围、截止与已确认链前缀,预算只可保持或提高。`action="abandon"` 关闭作业,`max_bytes` 必须等于原值;关闭后不删除文件,开启的计划可按到期时间生成新作业。原键重放仅返回原操作回执,不代表作业当前仍处于回执中的状态;随后查询列表确认实时状态。
|
||||||
|
|
||||||
|
## 4. 故障处理和文件核验
|
||||||
|
|
||||||
|
| 错误分类 | 处理方式 |
|
||||||
|
| --- | --- |
|
||||||
|
| STORAGE_LIMIT | 检查单作业预算和全卷预算;按需提高,或在运维确认备份后转移已经结清的文件。不会自动删除旧文件腾空间 |
|
||||||
|
| FILE_CHECK_FAILED | 检查原卷、目录、权限和文件完整性;恢复原字节后重试,禁止伪造摘要或跳过缺失批次 |
|
||||||
|
| STORAGE_BUSY | 同目录有其他 CLI/进程操作;核实旧执行者退出后重试 |
|
||||||
|
| POLICY_CHANGED | 原策略已变化或关闭;通常关闭旧作业,再按新版本保存计划 |
|
||||||
|
| PERMISSION_DENIED | 应用或命名空间停用;核查后按原状态重试或关闭 |
|
||||||
|
| EXPORT_FAILED | 数据库读取未完成;确认数据库恢复后用原作业重试 |
|
||||||
|
|
||||||
|
如果在首份清单落盘前进程退出、卷不可写或全卷预算耗尽,接管不会擅自创建一个新的空目录。此时可核查后关闭失败作业,让计划生成新作业。已有清单的容量故障可提高预算后重试原作业。
|
||||||
|
|
||||||
|
文件位于 `ORBIT_ARCHIVE_ROOT/{namespace_id}-{job_id}/`,清单及批次格式与 [文件归档 CLI](retention-jobs.md) 一致。数据库确认的链尾摘要可独立保存。服务端停止操作该目录后,可离线核验:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
orbit-admin retention verify-archive \
|
||||||
|
--directory /var/lib/orbit/archive/22222222222222222222222222222222-33333333333333333333333333333333 \
|
||||||
|
--expected-sha256 "$TRUSTED_SHA256"
|
||||||
|
```
|
||||||
|
|
||||||
|
全卷预算按普通文件的逻辑大小累计,单次扫描最多 100000 个目录项,包含未完成、失败与已关闭作业;超过边界停止。并发同卷执行受全卷文件锁约束。它不等价于文件系统物理空间配额,不包含块分配/元数据开销,也不能约束绕过平台直接写目录的外部进程。生产使用独立持久卷,并监测实际磁盘余量。暂不提供文件删除、自动迁移介质、下载正文或对象存储接口。
|
||||||
@@ -21,6 +21,23 @@ ORBIT_MYSQL_DSN 通过环境注入,命令不读取 .env。所有名称必填
|
|||||||
|
|
||||||
输出 JSON 包含 application_id / namespace_id / queue_id / credential_id / token / expires_at。初始凭据有效期为 30 天,权限包括当前命名空间内的五个 P1 动作和全部队列。初始队列 max_concurrency=4、rate_per_second=10、max_unfinished_tasks=100000;并发、开始限速与积压背压已生效,语义及管理方式见 [队列接口](queues.md)。这些默认值是开发基线,不是容量承诺。
|
输出 JSON 包含 application_id / namespace_id / queue_id / credential_id / token / expires_at。初始凭据有效期为 30 天,权限包括当前命名空间内的五个 P1 动作和全部队列。初始队列 max_concurrency=4、rate_per_second=10、max_unfinished_tasks=100000;并发、开始限速与积压背压已生效,语义及管理方式见 [队列接口](queues.md)。这些默认值是开发基线,不是容量承诺。
|
||||||
|
|
||||||
|
如果希望在第一次初始化时同时创建控制台管理员,可在同一条 bootstrap 事务中提供账号,并从标准输入交付密码:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
secret-manager read orbit/console-admin-password | \
|
||||||
|
go run ./cmd/orbit-admin bootstrap \
|
||||||
|
--application sample-app \
|
||||||
|
--namespace default \
|
||||||
|
--environment production \
|
||||||
|
--queue files \
|
||||||
|
--console-username admin \
|
||||||
|
--console-display-name 'Orbit 管理员' \
|
||||||
|
--console-password-stdin \
|
||||||
|
--output .local/production.credentials.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`--console-password-stdin` 要求密码至少 12 个字节,密码不会出现在命令参数、日志或交付 JSON 中。业务凭据、控制台账号和两条初始化审计会在同一个数据库事务中提交;任何一步失败都会整体回滚。交付 JSON 只会额外包含 `console_username`,后续新增控制台账号仍使用 [`orbit-admin console-user create`](../console.md)。
|
||||||
|
|
||||||
资源、凭据摘要和初始化审计在一个事务中提交。交付文件先写入并同步到磁盘,成功后再提交事务。如果文件写入失败则回滚并移除文件;如果已经交付后提交无法确认,保留文件并报错,应使用其中凭据核对结果,不能直接覆盖或重复创建。
|
资源、凭据摘要和初始化审计在一个事务中提交。交付文件先写入并同步到磁盘,成功后再提交事务。如果文件写入失败则回滚并移除文件;如果已经交付后提交无法确认,保留文件并报错,应使用其中凭据核对结果,不能直接覆盖或重复创建。
|
||||||
|
|
||||||
初始文件包含真实秘密,应保管在受限目录或导入秘密管理系统。运行账户与迁移/初始化账户分开;生产只通过 HTTPS 使用凭据。
|
初始文件包含真实秘密,应保管在受限目录或导入秘密管理系统。运行账户与迁移/初始化账户分开;生产只通过 HTTPS 使用凭据。
|
||||||
|
|||||||
@@ -157,4 +157,4 @@ for _, item := range result.Items {
|
|||||||
|
|
||||||
SDK验证输入数量/身份/整体体积及返回数量、顺序、状态和回执归属;不自动重试、拆批或创建新键。`err == nil` 只表示取得可解释的逐项响应。局部错误为 `BatchItemError`,不是顶层APIError;整批HTTP错误仍为APIError,网络错误沿用标准库。独立SDK无数据库依赖。
|
SDK验证输入数量/身份/整体体积及返回数量、顺序、状态和回执归属;不自动重试、拆批或创建新键。`err == nil` 只表示取得可解释的逐项响应。局部错误为 `BatchItemError`,不是顶层APIError;整批HTTP错误仍为APIError,网络错误沿用标准库。独立SDK无数据库依赖。
|
||||||
|
|
||||||
完整运行命令、输入输出和收尾见 [批量场景](../examples/batch.md),源码见 [SDK示例](../../sdk/go/examples/batch/main.go),覆盖范围和实际证据见 [回归报告](../reports/phase-3-batch-tasks.md)。旧客户端继续使用单项接口;新SDK调用旧服务该路由会返回404,不会静默退化为其他语义。
|
完整运行命令、输入输出和收尾见 [批量场景](../examples/batch.md),源码见 [SDK示例](../../examples/batch/main.go),覆盖范围和实际证据见 [回归报告](../reports/phase-3-batch-tasks.md)。旧客户端继续使用单项接口;新SDK调用旧服务该路由会返回404,不会静默退化为其他语义。
|
||||||
|
|||||||
@@ -88,13 +88,13 @@ return nil, &orbit.WorkerCancellationConfirmed{
|
|||||||
|
|
||||||
## 4. 可运行场景
|
## 4. 可运行场景
|
||||||
|
|
||||||
源码:[cancellation/main.go](../../sdk/go/examples/cancellation/main.go)。使用 [Worker 初始化](../sdk/worker-runtime.md) 的私有凭据文件,需 tasks:submit/read/cancel、workers:connect/consume 和队列范围。此示例只等待停止提示,没有外部业务副作用。
|
源码:[cancellation/main.go](../../examples/cancellation/main.go)。使用 [Worker 初始化](../sdk/worker-runtime.md) 的私有凭据文件,需 tasks:submit/read/cancel、workers:connect/consume 和队列范围。此示例只等待停止提示,没有外部业务副作用。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=/private/path/worker.credentials.json \
|
ORBIT_CREDENTIALS_FILE=/private/path/worker.credentials.json \
|
||||||
go run ./examples/cancellation
|
go run ./cancellation
|
||||||
```
|
```
|
||||||
|
|
||||||
预期输出:
|
预期输出:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 执行尝试与证据查询 v1
|
# 执行尝试与证据查询 v1
|
||||||
|
|
||||||
更新:2026-09-17。四个只读接口及独立 Go SDK 已实现;对应 [可运行示例](../../sdk/go/examples/history/main.go) 和 [回归记录](../reports/phase-2-execution-history.md)。
|
更新:2026-09-17。四个只读接口及独立 Go SDK 已实现;对应 [可运行示例](../../examples/history/main.go) 和 [回归记录](../reports/phase-2-execution-history.md)。
|
||||||
|
|
||||||
## 1. 用途与权限
|
## 1. 用途与权限
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ accepted, err := api.SubmitTask(ctx, "update-object-42-operation-001", orbit.Sub
|
|||||||
|
|
||||||
## 5. 完整可运行场景
|
## 5. 完整可运行场景
|
||||||
|
|
||||||
源码:[keyed-concurrency/main.go](../../sdk/go/examples/keyed-concurrency/main.go)。示例创建两个专用队列,每条队列提交一个相同资源键的任务;两个自动 Worker 各自容量为 4。处理器调用一个受控内存资源模型,查询尝试确认序号增长,最后模拟旧请求验证它被拒绝。
|
源码:[keyed-concurrency/main.go](../../examples/keyed-concurrency/main.go)。示例创建两个专用队列,每条队列提交一个相同资源键的任务;两个自动 Worker 各自容量为 4。处理器调用一个受控内存资源模型,查询尝试确认序号增长,最后模拟旧请求验证它被拒绝。
|
||||||
|
|
||||||
从仓库根目录,在专用开发数据库完成迁移并启动服务:
|
从仓库根目录,在专用开发数据库完成迁移并启动服务:
|
||||||
|
|
||||||
@@ -94,9 +94,9 @@ go run ./cmd/orbit
|
|||||||
上述命令使用预先设置的 `ORBIT_MYSQL_DSN`,服务使用 `ORBIT_HTTP_ADDR=127.0.0.1:8080`。示例凭据需具有全部队列范围的 queues:manage、tasks:submit/read 和 workers:connect/consume。另一个终端执行:
|
上述命令使用预先设置的 `ORBIT_MYSQL_DSN`,服务使用 `ORBIT_HTTP_ADDR=127.0.0.1:8080`。示例凭据需具有全部队列范围的 queues:manage、tasks:submit/read 和 workers:connect/consume。另一个终端执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=/tmp/orbit-keyed.credentials.json go run ./examples/keyed-concurrency
|
ORBIT_CREDENTIALS_FILE=/tmp/orbit-keyed.credentials.json go run ./keyed-concurrency
|
||||||
```
|
```
|
||||||
|
|
||||||
正常输出:
|
正常输出:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
`orbit-admin release manifest` 不连接数据库,输出当前二进制内置的构建身份、协议版本和完整迁移名称/校验和。它用于在构建阶段冻结发布证据;输出不含 DSN、凭据、迁移 SQL 或业务数据。
|
`orbit-admin release manifest` 不连接数据库,输出当前二进制内置的构建身份、协议版本和完整迁移名称/校验和。它用于在构建阶段冻结发布证据;输出不含 DSN、凭据、迁移 SQL 或业务数据。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run --rm --entrypoint /usr/local/bin/orbit-admin registry.example/orbit:1.0.0 release manifest > orbit-release-manifest.json
|
docker run --rm --entrypoint /usr/local/bin/orbit-admin registry.example/orbit:v0.1.1 release manifest > orbit-release-manifest.json
|
||||||
# 本地工作区也可执行:
|
# 本地工作区也可执行:
|
||||||
make release-manifest > orbit-release-manifest.json
|
make release-manifest > orbit-release-manifest.json
|
||||||
```
|
```
|
||||||
@@ -26,8 +26,8 @@ make release-manifest > orbit-release-manifest.json
|
|||||||
"workflow_schema_version": 1
|
"workflow_schema_version": 1
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"current_version": 28,
|
"current_version": 30,
|
||||||
"migration_count": 28,
|
"migration_count": 30,
|
||||||
"migrations": [
|
"migrations": [
|
||||||
{"version": 1, "name": "000001_initial.up.sql", "checksum": "<sha256>"}
|
{"version": 1, "name": "000001_initial.up.sql", "checksum": "<sha256>"}
|
||||||
]
|
]
|
||||||
@@ -47,7 +47,7 @@ ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit' \
|
|||||||
成功时输出类似:
|
成功时输出类似:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"schema_version":28,"migration_count":28,"foreign_key_count":0,"required_index_count":6,"complete":true}
|
{"schema_version":31,"migration_count":31,"foreign_key_count":0,"required_index_count":6,"complete":true}
|
||||||
```
|
```
|
||||||
|
|
||||||
`complete=true` 表示当前二进制的迁移账本、Orbit 表零外键,以及 000026–000028 的六个有界观测索引均满足要求。需要同时核对发布物身份时,传入保存的清单:
|
`complete=true` 表示当前二进制的迁移账本、Orbit 表零外键,以及 000026–000028 的六个有界观测索引均满足要求。需要同时核对发布物身份时,传入保存的清单:
|
||||||
@@ -79,3 +79,9 @@ ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit' \
|
|||||||
```
|
```
|
||||||
|
|
||||||
它与 `/metrics` 共享同一有界读取实现,但不会改变指标注册器或业务状态;遇到数据库超时会返回非零状态,调用方应保留上一次成功快照并排查数据库。
|
它与 `/metrics` 共享同一有界读取实现,但不会改变指标注册器或业务状态;遇到数据库超时会返回非零状态,调用方应保留上一次成功快照并排查数据库。
|
||||||
|
|
||||||
|
## 文件归档与离线核验
|
||||||
|
|
||||||
|
[`orbit-admin retention archive / verify-archive`](retention-jobs.md) 提供持久文件作业、原目录续跑及摘要链核验。读取命名空间需显式指定已保存策略版本,每个新批次记录独立作业审计,源记录不删除;离线核验无需数据库。
|
||||||
|
|
||||||
|
周期归档部署、持久卷及故障处理见[周期归档管理](archive-scheduler.md);需 Schema 31,默认关闭后台归档执行器。
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ Worker 连接不再各自直接并发发起一批数据库领取。节点采用
|
|||||||
|
|
||||||
## 4. SDK 完整场景
|
## 4. SDK 完整场景
|
||||||
|
|
||||||
源码:[priority/main.go](../../sdk/go/examples/priority/main.go)。创建专用队列,先按 0、9、4 的顺序提交三条任务,再启动容量为 1 的自动 Worker;三条任务在近同时到期的条件下按 9、4、0 执行,每条只有一次尝试。
|
源码:[priority/main.go](../../examples/priority/main.go)。创建专用队列,先按 0、9、4 的顺序提交三条任务,再启动容量为 1 的自动 Worker;三条任务在近同时到期的条件下按 9、4、0 执行,每条只有一次尝试。
|
||||||
|
|
||||||
从仓库根目录准备专用开发数据库,设置 ORBIT_MYSQL_DSN 和 `ORBIT_HTTP_ADDR=127.0.0.1:8080` 后:
|
从仓库根目录准备专用开发数据库,设置 ORBIT_MYSQL_DSN 和 `ORBIT_HTTP_ADDR=127.0.0.1:8080` 后:
|
||||||
|
|
||||||
@@ -83,9 +83,9 @@ go run ./cmd/orbit
|
|||||||
凭据需要全部队列范围的 queues:manage、tasks:submit/read、workers:connect/consume。另一个终端执行:
|
凭据需要全部队列范围的 queues:manage、tasks:submit/read、workers:connect/consume。另一个终端执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=/tmp/orbit-priority.credentials.json go run ./examples/priority
|
ORBIT_CREDENTIALS_FILE=/tmp/orbit-priority.credentials.json go run ./priority
|
||||||
```
|
```
|
||||||
|
|
||||||
预期输出:
|
预期输出:
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ SDK 返回 `*orbit.APIError`,Code=QUEUE_FULL、StatusCode=429、RetryAfter=tim
|
|||||||
|
|
||||||
## 完整 SDK 场景
|
## 完整 SDK 场景
|
||||||
|
|
||||||
源码:[resources/main.go](../../sdk/go/examples/resources/main.go)。先完成当前迁移并启动服务,初始化带 Worker 权限且 queues:manage 范围为全部队列的专用开发凭据:
|
源码:[resources/main.go](../../examples/resources/main.go)。先完成当前迁移并启动服务,初始化带 Worker 权限且 queues:manage 范围为全部队列的专用开发凭据:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mkdir -p .local
|
mkdir -p .local
|
||||||
@@ -120,10 +120,10 @@ chmod 700 .local
|
|||||||
go run ./cmd/orbit-admin bootstrap \
|
go run ./cmd/orbit-admin bootstrap \
|
||||||
--application resources-demo --namespace default --environment development \
|
--application resources-demo --namespace default --environment development \
|
||||||
--queue bootstrap --worker-access --output .local/resources.credentials.json
|
--queue bootstrap --worker-access --output .local/resources.credentials.json
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/resources.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/resources.credentials.json \
|
||||||
go run ./examples/resources
|
go run ./resources
|
||||||
```
|
```
|
||||||
|
|
||||||
示例创建独立队列(积压 2、并发 2、每秒 1 次开始),暂停后提交两个任务;第三个被拒,重复第一个仍得到原回执;取消第一个释放空间,用第三个原身份重新提交,然后恢复并执行两个纯数据回显任务。
|
示例创建独立队列(积压 2、并发 2、每秒 1 次开始),暂停后提交两个任务;第三个被拒,重复第一个仍得到原回执;取消第一个释放空间,用第三个原身份重新提交,然后恢复并执行两个纯数据回显任务。
|
||||||
|
|||||||
115
docs/api/retention-jobs.md
Normal file
115
docs/api/retention-jobs.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# 可恢复的文件归档作业
|
||||||
|
|
||||||
|
`orbit-admin retention archive` 自动按已保存的保留策略分批导出,并把每个批次可靠写入本地持久目录。它面向持有数据库运维权限的操作者,复用控制台导出的候选保护和版本核对,记录 `archive:<job_id>` 审计身份;不需要控制台 Cookie,也不向业务 SDK 增加管理权限。数据库业务记录始终保留。
|
||||||
|
|
||||||
|
本页介绍可独立运行的有限批次 CLI 执行器。周期触发、控制台作业列表与人工重试见[周期归档管理](archive-scheduler.md);对象存储和源数据清理尚未接入。
|
||||||
|
|
||||||
|
## 配置与启动
|
||||||
|
|
||||||
|
先在控制台“保留策略”保存所需窗口并允许导出,记下命名空间 ID 和策略版本。为每次扫描选择独立目录,父目录需提前创建,数据目录应位于受备份保护的持久卷中;进程会创建权限为 `0700` 的作业子目录和 `0600` 文件。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 按实际数据库注入 ORBIT_MYSQL_DSN,不要把密码放入版本库。
|
||||||
|
export ORBIT_MYSQL_DSN='operator:password@tcp(127.0.0.1:3306)/orbit'
|
||||||
|
mkdir -p /srv/orbit-archives
|
||||||
|
chmod 700 /srv/orbit-archives
|
||||||
|
|
||||||
|
./bin/orbit-admin retention archive \
|
||||||
|
--namespace 11111111111111111111111111111111 \
|
||||||
|
--kind tasks \
|
||||||
|
--revision 1 \
|
||||||
|
--directory /srv/orbit-archives/tasks-20260923 \
|
||||||
|
--batch-size 20 \
|
||||||
|
--max-batches 10 \
|
||||||
|
--max-bytes 268435456 \
|
||||||
|
--timeout 5m
|
||||||
|
```
|
||||||
|
|
||||||
|
ID、日期、版本均为示例,使用当前环境的值。目录也可通过 `ORBIT_ARCHIVE_DIRECTORY` 配置,显式 `--directory` 优先;该环境变量只供归档 CLI 使用,不会使 Orbit 服务后台自动开始归档。
|
||||||
|
|
||||||
|
| 参数 | 默认/约束 |
|
||||||
|
| --- | --- |
|
||||||
|
| `--namespace` | 必填,32 位小写十六进制命名空间 ID |
|
||||||
|
| `--kind` | 必填,`tasks/workflows/events/audit/deliveries/inbox/idempotency` |
|
||||||
|
| `--revision` | 必填,已保存的非零策略版本 |
|
||||||
|
| `--directory` | 必填绝对路径,或设置 `ORBIT_ARCHIVE_DIRECTORY`;每个作业独立私有目录 |
|
||||||
|
| `--batch-size` | 默认 20,1–100;与范围一同固定在清单,恢复时不可更改 |
|
||||||
|
| `--max-batches` | 默认 10,1–1000;本次调用最多新增的批次数 |
|
||||||
|
| `--max-bytes` | 默认 256 MiB,1 MiB–100 GiB;本作业清单及已发布批次文件的总上限 |
|
||||||
|
| `--timeout` | 默认 5 分钟,1 秒–1 小时;单批数据库读取仍受 10 秒截止及原有行数/正文限制 |
|
||||||
|
|
||||||
|
所有调用仍核对数据库 Schema;每个新批次在事务内核对应用、命名空间启用状态和当前策略版本。不能使用已关闭或改版的策略继续生成新批次。
|
||||||
|
|
||||||
|
输出为摘要,不包含正文、凭据或游标:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"job_id":"<固定作业ID>","completed":false,"batches":10,"roots":200,"bytes":1234567,"last_sha256":"<清单及批次链的末端摘要>"}
|
||||||
|
```
|
||||||
|
|
||||||
|
命令达到 `--max-batches` 后正常返回;`completed=false` 明确表示尚未导出完。使用**相同目录、命名空间、类型、版本和批量**再次运行,自动校验已有批次后接续。可提高本次批数/容量/时间上限,不会改写已发布文件。已完成作业再次运行只验证,不再从头导出。
|
||||||
|
|
||||||
|
作业执行中遇到数据库错误、策略变更、文件损坏、权限问题或容量不足时输出最后已确认进度并以非零状态退出。参数或数据库连接等启动前校验失败只报错,不输出成功状态;此时可用离线核验命令读取已保存进度。调用方必须同时检查退出码和 `completed`,不能把“进程结束”当成“作业完成”。
|
||||||
|
|
||||||
|
## 文件结构与恢复
|
||||||
|
|
||||||
|
```text
|
||||||
|
/srv/orbit-archives/tasks-20260923/
|
||||||
|
├── .lock # 非阻塞进程文件锁;进程退出后自动释放
|
||||||
|
├── manifest.json # 不可变范围、随机作业ID及清单版本
|
||||||
|
├── batch-000001.json # 输入游标、服务端原始快照、上一文件摘要
|
||||||
|
├── batch-000002.json
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
写入顺序是:数据库读取并记录导出审计 → 私有临时文件 → 文件同步 → 原子发布且拒绝覆盖 → 同步目录 → 原字节回读。没有独立的可变游标文件;连续且校验通过的批次本身就是检查点。
|
||||||
|
|
||||||
|
- 文件发布前退出:残留 `.write.tmp` 不算成功,恢复时清理该临时文件并重新导出同一输入游标。
|
||||||
|
- 文件发布后、进程报告成功前退出:新进程读取已发布批次,从其下一游标恢复。
|
||||||
|
- 缺少批次、摘要不一致、混入其他作业、符号链接、非私有文件或未知文件:立即停止,不跳过、不覆盖原档案。
|
||||||
|
- 数据库导出已记审计、随后磁盘写入失败:可能多出一次导出审计;审计不表示文件持久化成功。源数据不变,恢复后从最后文件继续。
|
||||||
|
- 相同目录的并发进程:后进入者立即失败,避免两个进程同时推进。
|
||||||
|
|
||||||
|
首批绑定清单原字节摘要,后续每批绑定前一文件的摘要,快照正文另按服务端 SHA-256 校验。保留窗口截止时间跨批固定;每批使用独立数据库快照,整个作业不承诺全库同一时间点。期间解除保护的旧记录可能需要下一次独立扫描。
|
||||||
|
|
||||||
|
## 离线核验
|
||||||
|
|
||||||
|
不需要数据库连接或网络:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./bin/orbit-admin retention verify-archive \
|
||||||
|
--directory /srv/orbit-archives/tasks-20260923
|
||||||
|
|
||||||
|
# 推荐另外保存成功输出中的 last_sha256,再以可信摘要核对完整链。
|
||||||
|
./bin/orbit-admin retention verify-archive \
|
||||||
|
--directory /srv/orbit-archives/tasks-20260923 \
|
||||||
|
--expected-sha256 '<另行保存的64位小写摘要>'
|
||||||
|
```
|
||||||
|
|
||||||
|
核验输出与执行输出结构相同;部分作业也可以核验,`completed=false` 时仍未完成。`--expected-sha256` 可发现文件与摘要整体重写或尾部截断;前提是期望摘要保存在档案目录之外的可信位置。未提供期望摘要时只能验证文件之间的内部一致性,不能证明来源真实性。
|
||||||
|
|
||||||
|
单批快照在 `batch-*.json` 的 `snapshot` 字段中,仍遵循[归档接口](retention.md)的 `format_version/sha256/content` 协议。
|
||||||
|
|
||||||
|
## Docker 运行
|
||||||
|
|
||||||
|
把持久卷挂载到容器目录,并确保镜像内 UID 65532 有权限创建 `0700` 子目录。数据库和路径按目标环境配置;使用当前服务相同的不可变镜像版本:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker run --rm --network orbit-network \
|
||||||
|
--env-file /secure/orbit-archive.env \
|
||||||
|
--mount type=bind,src=/srv/orbit-archives,dst=/archives \
|
||||||
|
--entrypoint /usr/local/bin/orbit-admin orbit:local retention archive \
|
||||||
|
--namespace 11111111111111111111111111111111 --kind tasks --revision 1 \
|
||||||
|
--directory /archives/tasks-20260923
|
||||||
|
|
||||||
|
# 离线核验也需要文件锁,挂载目录需允许打开锁文件;不连接数据库。
|
||||||
|
docker run --rm --network none \
|
||||||
|
--mount type=bind,src=/srv/orbit-archives,dst=/archives \
|
||||||
|
--entrypoint /usr/local/bin/orbit-admin orbit:local retention verify-archive \
|
||||||
|
--directory /archives/tasks-20260923
|
||||||
|
```
|
||||||
|
|
||||||
|
当前文件执行器支持 Linux/macOS 本地 POSIX 文件锁、硬链接原子发布及文件/目录同步语义;其他系统显式拒绝。不同节点独立磁盘不共享进度;网络共享文件系统必须验证上述语义后才能使用,当前没有跨节点自动接管承诺。
|
||||||
|
|
||||||
|
容量上限覆盖一个作业的清单和已发布批次,不是整个卷的配额;还需预留一个临时批次空间。已有档案不会自动删除腾空间,不会自动生成新目录重试。每个作业最多 10000 批。该归档不是数据库备份,也不构成源数据删除授权。
|
||||||
|
|
||||||
|
周期触发、控制台作业列表与人工重试已由 Schema 31 增加,见[周期归档管理](archive-scheduler.md)。本页 CLI 仍可独立使用;独立 CLI 作业不会自动注册到管理列表。
|
||||||
@@ -1,4 +1,119 @@
|
|||||||
# 保留清单(只读)
|
# 数据保留与归档策略
|
||||||
|
|
||||||
|
Orbit 将保留策略作为命名空间级数据库配置保存。控制台保持单管理员模型,登录后打开“保留策略”即可查看和修改窗口;每次修改都要求上一版本号、幂等键和变更原因,前后值写入 `orbit_audit_events`。多个 Orbit 节点读取同一个 MySQL 版本,因此不会因为节点环境变量不同而产生不同清理边界。
|
||||||
|
|
||||||
|
首次安装和数据库策略尚未保存时使用部署默认值:幂等记录 90 天、任务历史 365 天、流程历史 365 天、事件日志 365 天、已确认交付 90 天、已处理 Inbox 90 天、管理审计 2555 天。默认 `enabled=false`,保存策略也不会立即删除数据。
|
||||||
|
|
||||||
|
环境变量只负责首次展示/未保存策略的默认值:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ORBIT_RETENTION_IDEMPOTENCY_DAYS=90
|
||||||
|
ORBIT_RETENTION_TASK_DAYS=365
|
||||||
|
ORBIT_RETENTION_WORKFLOW_DAYS=365
|
||||||
|
ORBIT_RETENTION_EVENT_DAYS=365
|
||||||
|
ORBIT_RETENTION_DELIVERY_DAYS=90
|
||||||
|
ORBIT_RETENTION_INBOX_DAYS=90
|
||||||
|
ORBIT_RETENTION_AUDIT_DAYS=2555
|
||||||
|
```
|
||||||
|
|
||||||
|
窗口范围为 1–3650 天,幂等记录不得短于 90 天;所有节点应使用同一组默认值。数据库中已有策略后,环境变量不覆盖它。管理端的“允许导出只读归档”控制导出权限,导出仍须核对固定策略版本。
|
||||||
|
|
||||||
|
归档分两步:先用“预览”或 `GET /console/retention/preview` 查看各类候选,再按固定类别调用 `POST /console/retention/export` 分批生成只读 JSON 快照。每个快照包含冻结的策略版本、数据库时间、截止时间、根记录及关联历史,并返回 SHA-256;源表仍然保留,导出成功只记录摘要和数量审计,不代表可以删除。后续删除执行器必须在独立发布和备份恢复验收后接入,不能把导出接口当成删除接口。
|
||||||
|
|
||||||
|
候选筛选由服务端固定目录实现,浏览器不能提交表名或 SQL。终态任务必须没有未知效果、租约和流程所有权;流程必须终态且没有未结任务或未处理 Inbox;事件必须没有未确认交付或未处理 Inbox;交付只允许已确认记录;Inbox 只允许已处理且流程已终态记录;幂等记录需同时超过过期时间和窗口。审计单独使用审计窗口,普通清理不处理审计记录。未确认交付、未处理 Inbox、未知效果、进行中的任务/流程和有效幂等身份始终受保护。
|
||||||
|
|
||||||
|
控制台接口示例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 已登录控制台会话;浏览器通常直接使用页面,不需要手工传令牌
|
||||||
|
curl -b orbit-console.cookie http://127.0.0.1:8080/console/retention
|
||||||
|
curl -b orbit-console.cookie -X PUT http://127.0.0.1:8080/console/retention \
|
||||||
|
-H 'Content-Type: application/json' -H 'Idempotency-Key: retention-2026-q3' \
|
||||||
|
--data '{"expected_revision":"0","enabled":false,"idempotency_days":90,"task_history_days":365,"workflow_history_days":365,"event_log_days":365,"delivery_history_days":90,"inbox_history_days":90,"audit_log_days":2555,"reason":"初始生产保留评审"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 管理页操作
|
||||||
|
|
||||||
|
1. 登录后打开 `/console/#/retention`,调整窗口、填写单行原因后保存;原因最多 1024 UTF-8 字节。幂等记录最少 90 天。
|
||||||
|
2. 点击“预览候选”:每类最多展示 100 条,`100+` 表示还有更多,并非全库总数。修改未保存时不能预览或导出,避免使用错误版本。
|
||||||
|
3. 需要导出时开启“允许导出只读归档”并保存,选择历史类型,点击“导出首批”。每批最多 20 条根记录,关联历史一并导出。浏览器需要 HTTPS 或 localhost 安全上下文来执行 SHA-256 校验。
|
||||||
|
4. 页面显示本批、累计条数和完整摘要;有后续记录时显示“导出下一批”,到末批后停止。可“重新下载本批”,切换历史类型或保存新策略会重置进度。“从首批开始”重新扫描,不会清理已下载文件。
|
||||||
|
|
||||||
|
下载文件保存 `format_version`、`sha256`、原始字符串 `content` 三个字段。页面关闭或刷新后内存中的分页进度会丢失;文件中的 `content.next_cursor` 可用于接口继续导出。服务端提交后网络中断可能多记录一次导出审计,但不改变任何源业务记录;页面未成功校验时不推进游标。修改策略的网络重试在当前页面复用原幂等键。
|
||||||
|
|
||||||
|
## 导出接口与分页
|
||||||
|
|
||||||
|
这些接口仅允许已登录控制台会话;业务 Bearer 凭据不能代替管理员会话。
|
||||||
|
|
||||||
|
| 接口 | 请求/结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| `GET /console/retention` | `data` 为当前策略,`revision` 是字符串;未保存版本为 `"0"` |
|
||||||
|
| `PUT /console/retention` | 必填全部窗口、`enabled`、字符串 `expected_revision`、单行 `reason`,请求头必填 `Idempotency-Key`;幂等返回同一版本 |
|
||||||
|
| `GET /console/retention/preview` | `data.policy` 为所用策略,`sample_limit=100`;各类别包含 `kind/cutoff/count/more` |
|
||||||
|
| `POST /console/retention/export` | `kind`、字符串 `expected_revision` 必填;`limit` 默认 20、最大 100;`cursor` 默认空 |
|
||||||
|
|
||||||
|
响应采用标准 `{"data": ...}` 信封。导出请求示例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -b orbit-console.cookie http://127.0.0.1:8080/console/retention/export \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data '{"kind":"tasks","expected_revision":"1","limit":20}' \
|
||||||
|
--output export-response.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`data.content` 是原始 JSON 文本,解析后包括:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `policy` / `kind` | 本批实际策略与根记录类别 |
|
||||||
|
| `snapshot_at` / `cutoff` | 数据库读取时间与严格早于的候选截止时间,UTC |
|
||||||
|
| `source_retained` | 始终为 `true`,源数据保留 |
|
||||||
|
| `root_count` | 本批根记录数量,不含关联历史计数 |
|
||||||
|
| `tables` | 固定目录下的表与行;二进制为十六进制,整数和 JSON 正文按文本保留精度 |
|
||||||
|
| `next_cursor` | 非空表示可以继续;下一次请求原样传入相同 `kind` 和 `expected_revision` |
|
||||||
|
|
||||||
|
游标绑定命名空间、历史类型、策略版本与截止时间;不接受客户端表名或 SQL。策略变化后必须重新核对并从首批开始。每批使用独立数据库一致性快照,多个批次不是同一个全库时间点;期间从受保护状态转为候选的记录可能需要重新扫描。归档不是灾难恢复备份,不能直接把这些 JSON 导入生产库。
|
||||||
|
|
||||||
|
每个导出事务最多 10 秒、10000 条关联记录、8 MiB 正文;超限整体失败,不返回截断快照。减少根记录 `limit` 后可重新尝试;单个根记录本身过大时需要另行处理,不能忽略其历史。每次成功导出记录类别、策略版本、数量和摘要审计。
|
||||||
|
|
||||||
|
| 情况 | 处理 |
|
||||||
|
| --- | --- |
|
||||||
|
| 未登录/会话已失效 | 401,重新登录 |
|
||||||
|
| 归档关闭或策略版本不匹配 | 409,读取策略并重新核对 |
|
||||||
|
| 不支持的类别、游标不匹配、数量越界、快照超限 | 400,修正参数;超限时减小批量 |
|
||||||
|
| 修改策略结果不明 | 使用完全相同的内容和 `Idempotency-Key` 重试 |
|
||||||
|
|
||||||
|
离线核验及生成下一批请求(兼容控制台下载信封和上述 curl 响应):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 控制台下载文件也可替换这里的文件路径。
|
||||||
|
response = json.loads(Path("export-response.json").read_text(encoding="utf-8"))
|
||||||
|
snapshot = response.get("data", response)
|
||||||
|
assert snapshot["format_version"] == 1
|
||||||
|
content = snapshot["content"].encode("utf-8")
|
||||||
|
assert hashlib.sha256(content).hexdigest() == snapshot["sha256"], "快照校验失败"
|
||||||
|
page = json.loads(content)
|
||||||
|
assert page["source_retained"] is True
|
||||||
|
print("本批根记录:", page["root_count"])
|
||||||
|
if page["next_cursor"]:
|
||||||
|
Path("next-request.json").write_text(json.dumps({
|
||||||
|
"kind": page["kind"], "expected_revision": page["policy"]["revision"],
|
||||||
|
"limit": 20, "cursor": page["next_cursor"],
|
||||||
|
}), encoding="utf-8")
|
||||||
|
# 下一批:curl ... --data-binary @next-request.json --output export-response-2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
摘要针对 `content` 的原始 UTF-8 字节,不对解析后重新格式化的 JSON 计算。SHA-256 用于发现内容损坏;摘要与正文一起被篡改时无法证明真实性,需要把审计摘要或外部签名另行保存在可信介质中。
|
||||||
|
|
||||||
|
## 自动分批与持久归档
|
||||||
|
|
||||||
|
需要在页面之外连续处理并断点续跑时,使用 [`orbit-admin retention archive`](retention-jobs.md)。目录、批量、总字节及执行时限可配置,已发布文件就是恢复检查点,支持离线核验;源业务数据仍保留。周期触发和清理执行器尚未接入。
|
||||||
|
|
||||||
|
## 只读盘点
|
||||||
|
|
||||||
`orbit-admin retention inventory` 用于在制定保留窗口、归档和备份恢复方案前盘点历史数据。它只执行带截止时间的统计查询,不删除、搬迁、改写数据,也不会自动判断某条记录可以安全归档。
|
`orbit-admin retention inventory` 用于在制定保留窗口、归档和备份恢复方案前盘点历史数据。它只执行带截止时间的统计查询,不删除、搬迁、改写数据,也不会自动判断某条记录可以安全归档。
|
||||||
|
|
||||||
|
|||||||
@@ -92,13 +92,13 @@ WorkerFailure 默认 Retryable=false;普通 error、panic、runtime.Goexit 和
|
|||||||
|
|
||||||
## 5. 运行完整示例
|
## 5. 运行完整示例
|
||||||
|
|
||||||
源码:[examples/retry/main.go](../../sdk/go/examples/retry/main.go)。先运行当前迁移、服务,并按 [Worker 初始化步骤](../sdk/worker.md#1-可运行的文本规范化示例) 准备具有提交、读取、消费权限的专用凭据。从仓库根目录执行:
|
源码:[examples/retry/main.go](../../examples/retry/main.go)。先运行当前迁移、服务,并按 [Worker 初始化步骤](../sdk/worker.md#1-可运行的文本规范化示例) 准备具有提交、读取、消费权限的专用凭据。从仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/worker.credentials.json \
|
||||||
go run ./examples/retry
|
go run ./retry
|
||||||
```
|
```
|
||||||
|
|
||||||
输入为 `{"text":" safe retry "}`。第一次处理在产生效果前模拟明确拒绝,第二次执行真正的文本规范化;重试等待由平台持久化安排。
|
输入为 `{"text":" safe retry "}`。第一次处理在产生效果前模拟明确拒绝,第二次执行真正的文本规范化;重试等待由平台持久化安排。
|
||||||
|
|||||||
@@ -178,13 +178,13 @@ go run ./cmd/orbit-admin bootstrap \
|
|||||||
--application schedule-demo --namespace default --environment development \
|
--application schedule-demo --namespace default --environment development \
|
||||||
--queue snapshots --worker-access --schedule-access \
|
--queue snapshots --worker-access --schedule-access \
|
||||||
--output .local/schedule.credentials.json
|
--output .local/schedule.credentials.json
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/schedule.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/schedule.credentials.json \
|
||||||
go run ./examples/schedules
|
go run ./schedules
|
||||||
```
|
```
|
||||||
|
|
||||||
前置数据库与目录步骤见 [SDK 接入](../sdk/README.md)。源码:[完整调度示例](../../sdk/go/examples/schedules/main.go)。示例创建暂停的每秒计划,恢复后让自动 Worker 返回对象输入快照,查询两次完成,再暂停并排空。计划本身有十秒结束边界,任务与历史保留;重复运行生成独立身份,不调用外部业务系统。
|
前置数据库与目录步骤见 [SDK 接入](../sdk/README.md)。源码:[完整调度示例](../../examples/schedules/main.go)。示例创建暂停的每秒计划,恢复后让自动 Worker 返回对象输入快照,查询两次完成,再暂停并排空。计划本身有十秒结束边界,任务与历史保留;重复运行生成独立身份,不调用外部业务系统。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
source_kind=SCHEDULE fixed_version=1
|
source_kind=SCHEDULE fixed_version=1
|
||||||
|
|||||||
@@ -322,6 +322,6 @@ _, err = client.ConfigureSubscription(ctx, current.ID, pauseKey, orbit.Configure
|
|||||||
return err
|
return err
|
||||||
```
|
```
|
||||||
|
|
||||||
[完整管理示例](../../sdk/go/examples/subscriptions/main.go) 的启动、输入输出、失败和保留规则见 [场景文档](../examples/subscriptions.md)。它演示管理与幂等,不启动结果消费者。正式 [事件连接 SDK 与自动消费器](../sdk/event-consumer.md) 已提供接收、ACK、心跳、重连与有界收尾;[完整协议示例](../examples/event-protocol.md) 验证跨连接重报原确认。[独立 MySQL Inbox 示例](../examples/result-inbox.md) 已实现并验证消费者进程退出后的去重。
|
[完整管理示例](../../examples/subscriptions/main.go) 的启动、输入输出、失败和保留规则见 [场景文档](../examples/subscriptions.md)。它演示管理与幂等,不启动结果消费者。正式 [事件连接 SDK 与自动消费器](../sdk/event-consumer.md) 已提供接收、ACK、心跳、重连与有界收尾;[完整协议示例](../examples/event-protocol.md) 验证跨连接重报原确认。[独立 MySQL Inbox 示例](../examples/result-inbox.md) 已实现并验证消费者进程退出后的去重。
|
||||||
|
|
||||||
机器可读消息契约见 [events.schema.json](../../protocol/eventsv1/events.schema.json),使用 Draft 2020-12,clientFrame/serverFrame 分方向约束。运行时额外拒绝重复 JSON 键、超范围 uint64 及非规范 base64url。Schema 不代替授权、状态机、租约及事务校验。
|
机器可读消息契约见 [events.schema.json](../../protocol/eventsv1/events.schema.json),使用 Draft 2020-12,clientFrame/serverFrame 分方向约束。运行时额外拒绝重复 JSON 键、超范围 uint64 及非规范 base64url。Schema 不代替授权、状态机、租约及事务校验。
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ for {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
片段假设已有客户端及time.Time类型from/before;完整可运行源码见[示例](../../sdk/go/examples/task-query/main.go)。SDK校验ID、范围、状态、键长度/UTF-8及时间精度,并用url.Values编码,时间转UTC,不修改调用方时间对象。超时只影响读取,可保持条件重新请求;没有自动重试或自动翻页。
|
片段假设已有客户端及time.Time类型from/before;完整可运行源码见[示例](../../examples/task-query/main.go)。SDK校验ID、范围、状态、键长度/UTF-8及时间精度,并用url.Values编码,时间转UTC,不修改调用方时间对象。超时只影响读取,可保持条件重新请求;没有自动重试或自动翻页。
|
||||||
|
|
||||||
## 5. 存储、升级与边界
|
## 5. 存储、升级与边界
|
||||||
|
|
||||||
|
|||||||
@@ -132,10 +132,10 @@ SDK的ScheduleVersion为uint64,JSON仍为字符串;时间使用time.Time。S
|
|||||||
|
|
||||||
| 场景 | 完整源码 | 运行说明与新增输出 |
|
| 场景 | 完整源码 | 运行说明与新增输出 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 单项受理、取消后读取来源 | [tasks](../../sdk/go/examples/tasks/main.go) | [SDK初始化](../sdk/README.md),source_kind=DIRECT |
|
| 单项受理、取消后读取来源 | [tasks](../../examples/tasks/main.go) | [SDK初始化](../sdk/README.md),source_kind=DIRECT |
|
||||||
| 按计划找到触发任务,再反查来源 | [schedules](../../sdk/go/examples/schedules/main.go) | [计划场景](schedules.md#6-sdk-与完整示例),source_kind=SCHEDULE、fixed_version=1 |
|
| 按计划找到触发任务,再反查来源 | [schedules](../../examples/schedules/main.go) | [计划场景](schedules.md#6-sdk-与完整示例),source_kind=SCHEDULE、fixed_version=1 |
|
||||||
| 人工核查后创建关联新任务 | [recovery](../../sdk/go/examples/recovery/main.go) | [恢复场景](../examples/recovery.md),source_kind=MANUAL_RETRY、parent=原任务ID |
|
| 人工核查后创建关联新任务 | [recovery](../../examples/recovery/main.go) | [恢复场景](../examples/recovery.md),source_kind=MANUAL_RETRY、parent=原任务ID |
|
||||||
| 并行流程完成后核对每项任务来源 | [workflow](../../sdk/go/examples/workflow/main.go) | [流程场景](../examples/workflow.md),workflow_sources_verified=3、fixed_version=v1 |
|
| 并行流程完成后核对每项任务来源 | [workflow](../../examples/workflow/main.go) | [流程场景](../examples/workflow.md),workflow_sources_verified=3、fixed_version=v1 |
|
||||||
|
|
||||||
上述示例均运行在隔离回归中;新发布计划/流程版本后老任务仍指向原版本,人工恢复后旧激活仍可追溯。WORKFLOW是新增来源种类,旧SDK可能按未知种类返回错误;应升级SDK,不能降级为DIRECT。不更改Task/TaskSummary或Worker消息结构,不增加权限配置;新客户端调用旧节点的404/503同样不能理解为DIRECT。
|
上述示例均运行在隔离回归中;新发布计划/流程版本后老任务仍指向原版本,人工恢复后旧激活仍可追溯。WORKFLOW是新增来源种类,旧SDK可能按未知种类返回错误;应升级SDK,不能降级为DIRECT。不更改Task/TaskSummary或Worker消息结构,不增加权限配置;新客户端调用旧节点的404/503同样不能理解为DIRECT。
|
||||||
|
|
||||||
|
|||||||
@@ -203,4 +203,4 @@ SDK:`Client.CancelTask(ctx, taskID, key, request)`。此操作不承诺回滚
|
|||||||
{"request_id":"44444444444444444444444444444444","error":{"code":"IDEMPOTENCY_CONFLICT","message":"idempotency key was used with different content"}}
|
{"request_id":"44444444444444444444444444444444","error":{"code":"IDEMPOTENCY_CONFLICT","message":"idempotency key was used with different content"}}
|
||||||
```
|
```
|
||||||
|
|
||||||
有限自动重试已实现,具体分类、退避、回执和升级边界见 [安全重试](retries.md)。[运行中取消](cancellation.md) 已实现;[批量受理](batch-tasks.md) 已实现逐项结果与原身份补交;[独立任务核查与关联新任务重试](task-recovery.md) 已实现。[执行历史](execution-history.md) 已提供尝试与证据的列表和详情。完整可运行程序见 [SDK 任务示例](../../sdk/go/examples/tasks/main.go),已验证异常场景见 [P1 回归报告](../reports/phase-1.md) 及 [取消回归](../reports/phase-3-cancellation.md)。SDK 忽略未知响应字段;更改已有字段语义需升级契约和回归。
|
有限自动重试已实现,具体分类、退避、回执和升级边界见 [安全重试](retries.md)。[运行中取消](cancellation.md) 已实现;[批量受理](batch-tasks.md) 已实现逐项结果与原身份补交;[独立任务核查与关联新任务重试](task-recovery.md) 已实现。[执行历史](execution-history.md) 已提供尝试与证据的列表和详情。完整可运行程序见 [SDK 任务示例](../../examples/tasks/main.go),已验证异常场景见 [P1 回归报告](../reports/phase-1.md) 及 [取消回归](../reports/phase-3-cancellation.md)。SDK 忽略未知响应字段;更改已有字段语义需升级契约和回归。
|
||||||
|
|||||||
79
docs/console.md
Normal file
79
docs/console.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Orbit Console · 星轨控制台
|
||||||
|
|
||||||
|
Orbit 内置了一个不依赖 Node.js、外部 CDN 或额外文件挂载的管理控制台第一版。静态资源编译进 Orbit 二进制,入口为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8080/console/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前能力
|
||||||
|
|
||||||
|
- 读取 `/health/live`、`/health/ready` 和 `/version` 展示服务、Schema 与工具链状态。
|
||||||
|
- 通过数据库中的控制台账号登录;服务端使用 HttpOnly 会话 Cookie 调用现有授权 API,浏览器不接触业务 Token。
|
||||||
|
- 总览页直接展示当前查询窗口的任务成功率、任务状态分布、活跃计划、待核查流程和待交付结果;登录和权限信息不会占用统计区域。
|
||||||
|
- 展示任务、定时计划、流程实例和结果订阅的最近摘要。
|
||||||
|
- 点击任务查看任务详情、执行尝试时间线、最终结果和操作审计摘要。
|
||||||
|
- 在具备对应权限时提交任务取消和安全重试,沿用服务端的版本校验、幂等键、队列范围和审计规则。
|
||||||
|
- 点击定时计划查看当前版本、规则、下一次触发和触发记录;在具备 `schedules:manage` 权限时可暂停或恢复计划。
|
||||||
|
- 点击流程实例查看当前权威状态、固定版本、步骤状态和事件时间线;步骤读取沿用 `workflows:read`,事件时间线没有 `workflows:audit:read` 时会明确显示权限提示。
|
||||||
|
- 点击结果订阅查看冻结配置、游标、积压窗口和最近交付历史;页面不展示租约秘密,也不绕过 `events:read` / `events:manage` 权限。
|
||||||
|
- 在具备 `workflows:control` 权限时,流程详情可以暂停、恢复或取消;操作原因、`control_revision` 和幂等键交给服务端做最终校验。
|
||||||
|
- `NEEDS_REVIEW` 流程会显示按步骤类型划分的人工恢复入口:任务核查、步骤结论、继续、步骤重试、PENDING 接续和等待重建;每次提交都要求当前修订、证据引用和显式确认,服务端恢复 API 负责最终门控。
|
||||||
|
- 在具备 `events:manage` 权限时,订阅详情可以启停订阅,并对 `EXPIRED` 交付追加次数预算后重试;原交付身份、已用次数和事件不会被页面重置。
|
||||||
|
- 左侧六个模块是独立的控制台视图,使用 `/console/#/overview`、`#/tasks`、`#/schedules`、`#/workflows`、`#/subscriptions` 和 `#/retention` 定位;切换视图不会把所有模块堆在一张长页面上。
|
||||||
|
- “保留策略”只允许单管理员查看和修改命名空间级窗口;修改需要版本号、幂等键和原因并留下审计。预览和只读导出不会删除源数据,未确认交付、未知效果和未结束流程不会成为候选。
|
||||||
|
- 任务状态按 10 秒自动刷新;页面只调用服务端授权接口,不把账号密码或业务凭据上传到独立服务,也不读取数据库。
|
||||||
|
- 页面使用当前请求方的业务权限;没有权限的模块显示为空或等待权限,不绕过服务端授权。
|
||||||
|
|
||||||
|
## 本地打开
|
||||||
|
|
||||||
|
启动 Orbit 后访问 `/console/`。未登录时,任何控制台业务页面都会跳转到 `/console/login`;登录成功后进入控制台,登录会话过期或服务端返回 401 时也会重新跳转。当前 Docker 开发环境入口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8080/console/
|
||||||
|
```
|
||||||
|
|
||||||
|
首次使用可以在 `orbit-admin bootstrap` 时一起创建控制台管理员。这样业务资源、业务凭据和管理员账号在同一个初始化事务中提交:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
secret-manager read orbit/console-admin-password | \
|
||||||
|
./bin/orbit-admin bootstrap \
|
||||||
|
--application sample-app \
|
||||||
|
--namespace default \
|
||||||
|
--environment production \
|
||||||
|
--queue files \
|
||||||
|
--console-username admin \
|
||||||
|
--console-display-name 'Orbit 管理员' \
|
||||||
|
--console-password-stdin \
|
||||||
|
--output .local/production.credentials.json
|
||||||
|
```
|
||||||
|
|
||||||
|
密码不会进入命令参数、日志或 JSON 文件,初始化失败时资源和账号一起回滚。已经完成业务初始化的环境,也可以由数据库管理员单独创建控制台账号。账号绑定一个已有的命名空间凭据,账号权限就是该凭据的权限;这不会轮换或输出业务 Token。推荐从私有凭据文件读取命名空间和凭据标识,并从标准输入交付密码:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./bin/orbit-admin console-user create \
|
||||||
|
--credential-file .local/local.credentials.json \
|
||||||
|
--username operator \
|
||||||
|
--display-name 'Orbit 管理员' \
|
||||||
|
--password-stdin
|
||||||
|
```
|
||||||
|
|
||||||
|
命令会把密码写成 bcrypt 哈希,只输出账号的脱敏信息。生产环境应通过受保护的密钥管道提供标准输入;不要把密码写进命令历史、镜像或仓库。账号创建后直接打开控制台,用账号密码登录。
|
||||||
|
|
||||||
|
页面的 API 地址默认自动取当前访问地址的 origin,不需要手填。部署到域名后,直接访问 `https://orbit.example.com/console/`,页面会自动请求同一域名下的服务端接口;生产建议用反向代理让控制台和 API 保持同源。页面不再要求填写队列 ID、Bearer Token,也不再选择凭据 JSON。
|
||||||
|
|
||||||
|
如果未来把前端静态资源和服务端拆成不同域名,需要额外设计 API 基地址、CORS、Cookie `SameSite`/Domain 和 CSRF 防护,不能只替换一个 URL。当前内置控制台优先保证同源部署的安全边界。
|
||||||
|
|
||||||
|
`ORBIT_CONSOLE_QUEUE_ID` 只作为单一默认队列的展示和查询范围配置:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export ORBIT_CONSOLE_QUEUE_ID='queue_xxx'
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /console/config` 只对已登录会话返回这个非敏感的默认队列 ID。健康检查和版本探针保持公开,业务 API、控制台 HTML、配置和所有业务模块均受登录会话与原有权限双重保护。
|
||||||
|
|
||||||
|
## 安全边界
|
||||||
|
|
||||||
|
控制台不直接访问数据库,也不绕过业务凭据权限。登录会话只保存令牌摘要,服务端每次请求都会校验账号、会话、命名空间、应用启停和凭据权限。取消和人工重试会使用当前任务版本生成一次性幂等键;网络结果不明时不要重复点击,应使用原始操作身份通过 API/SDK 核对。`NEEDS_REVIEW` 任务仍须使用带证据的核查 API/SDK 完成最终裁决。控制台账号绑定业务凭据,不自动获得跨命名空间权限;不支持跨命名空间聚合,不代替平台的权限与审计 API。
|
||||||
|
|
||||||
|
不能把业务 Token 长期放进浏览器本地存储,也不能让控制台直连 MySQL。计划版本发布、跨命名空间管理身份和其他高风险写操作仍应通过受保护 API/SDK 设计后再接入页面。流程恢复入口只调用已经定义的恢复 API,不自动猜测结果、不替操作者补齐证据,也不把冲突重试成“最新版本”。
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
# 通用场景示例
|
# 通用场景示例
|
||||||
|
|
||||||
|
完整源码已统一放在根目录 [examples/](../../examples/README.md),共有 27 个可运行场景;各目录 README 提供命令和预期结果。首次接入先看[仓库 README](../../README.md)。
|
||||||
|
|
||||||
示例只使用通用的作业、对象、文件、资源与审核信号,不包含接入方项目名称或行业规则。
|
示例只使用通用的作业、对象、文件、资源与审核信号,不包含接入方项目名称或行业规则。
|
||||||
|
|
||||||
## 已可运行:研发定义与发布流程
|
## 已可运行:研发定义与发布流程
|
||||||
|
|
||||||
- [完整命令和结果](workflow-definition.md)、[SDK源码](../../sdk/go/examples/workflow-definition/main.go)。
|
- [完整命令和结果](workflow-definition.md)、[SDK源码](../../examples/workflow-definition/main.go)。
|
||||||
- 六类节点的有限结构定义、原键及同版本确认、新版本发布、旧版本读取保持不变。
|
- 六类节点的有限结构定义、原键及同版本确认、新版本发布、旧版本读取保持不变。
|
||||||
- 本例只发布定义;实际运行见下方流程示例。
|
- 本例只发布定义;实际运行见下方流程示例。
|
||||||
|
|
||||||
## 已可运行:并行作业、定时等待与外部回执
|
## 已可运行:并行作业、定时等待与外部回执
|
||||||
|
|
||||||
- [完整命令、输入输出与失败处理](workflow.md)、[独立SDK源码](../../sdk/go/examples/workflow/main.go)。
|
- [完整命令、输入输出与失败处理](workflow.md)、[独立SDK源码](../../examples/workflow/main.go)。
|
||||||
- 两个任务并行处理并汇合,计时等待后接收显式目标代次的信号,再执行后续任务;最终三个任务完成并有界排空。
|
- 两个任务并行处理并汇合,计时等待后接收显式目标代次的信号,再执行后续任务;最终三个任务完成并有界排空。
|
||||||
- 启动及信号原键重复不会建立新的实例或信号;分页查看原状态事件,核对信号消费及赢家等待。
|
- 启动及信号原键重复不会建立新的实例或信号;分页查看原状态事件,核对信号消费及赢家等待。
|
||||||
|
|
||||||
@@ -18,6 +20,10 @@
|
|||||||
|
|
||||||
独立的流程场景另见[有界轮询](workflow-poll.md)与[证据恢复](workflow-recovery.md):前者区分请求重试与业务轮次,后者实际注入外部提交后HTTP断线,保留UNKNOWN历史和暂停门控。本机渠道是受控夹具,不能代替持久业务证据。
|
独立的流程场景另见[有界轮询](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
|
## 已可运行:业务事务与任务提交Outbox
|
||||||
|
|
||||||
- 独立模块:[task-outbox](../../examples/task-outbox/README.md),[完整前置、命令、Go方法及故障处理](task-outbox.md)。
|
- 独立模块:[task-outbox](../../examples/task-outbox/README.md),[完整前置、命令、Go方法及故障处理](task-outbox.md)。
|
||||||
@@ -28,13 +34,13 @@
|
|||||||
|
|
||||||
## 已可运行:相对延迟执行
|
## 已可运行:相对延迟执行
|
||||||
|
|
||||||
- [完整命令、输入输出和失败处理](delayed-task.md),[SDK源码](../../sdk/go/examples/delayed-task/main.go)。
|
- [完整命令、输入输出和失败处理](delayed-task.md),[SDK源码](../../examples/delayed-task/main.go)。
|
||||||
- delay_ms按数据库首次受理时间计时,实际Worker执行后查询开始时间;重报原请求仍返回原任务及时间。
|
- delay_ms按数据库首次受理时间计时,实际Worker执行后查询开始时间;重报原请求仍返回原任务及时间。
|
||||||
- 自动失败重试单独使用retry策略,初始延迟不在每次尝试中重新计时。
|
- 自动失败重试单独使用retry策略,初始延迟不在每次尝试中重新计时。
|
||||||
|
|
||||||
## 已可运行:健康检查
|
## 已可运行:健康检查
|
||||||
|
|
||||||
- 源码:[SDK 健康检查](../../sdk/go/examples/health/main.go)。
|
- 源码:[SDK 健康检查](../../examples/health/main.go)。
|
||||||
- 前置:已迁移的 MySQL 与已启动的基础服务。
|
- 前置:已迁移的 MySQL 与已启动的基础服务。
|
||||||
- 操作与预期输出:[SDK 接入说明](../sdk/README.md)。
|
- 操作与预期输出:[SDK 接入说明](../sdk/README.md)。
|
||||||
- 异常:数据库不可用或迁移记录异常时 readiness 返回 HTTP 503,SDK 返回类型化状态错误。
|
- 异常:数据库不可用或迁移记录异常时 readiness 返回 HTTP 503,SDK 返回类型化状态错误。
|
||||||
@@ -42,7 +48,7 @@
|
|||||||
|
|
||||||
## 已可运行:文件转换任务的受理与取消
|
## 已可运行:文件转换任务的受理与取消
|
||||||
|
|
||||||
- 源码:[SDK 任务受理示例](../../sdk/go/examples/tasks/main.go)。
|
- 源码:[SDK 任务受理示例](../../examples/tasks/main.go)。
|
||||||
- 前置:完成迁移、初始化凭据、启动服务,在没有 Worker 消费的专用队列运行此受理/取消示例,详见 [SDK 接入](../sdk/README.md)。
|
- 前置:完成迁移、初始化凭据、启动服务,在没有 Worker 消费的专用队列运行此受理/取消示例,详见 [SDK 接入](../sdk/README.md)。
|
||||||
- 输入:对象引用、格式与精确整数;提交两次同一身份,再查询、分页并带版本取消。
|
- 输入:对象引用、格式与精确整数;提交两次同一身份,再查询、分页并带版本取消。
|
||||||
- 预期:重复请求返回同一任务,最终 CANCELLED/revision=2,source_kind=DIRECT。当前不启动 Worker,不实际生成文件。
|
- 预期:重复请求返回同一任务,最终 CANCELLED/revision=2,source_kind=DIRECT。当前不启动 Worker,不实际生成文件。
|
||||||
@@ -51,7 +57,7 @@
|
|||||||
|
|
||||||
## 已可运行:文件格式转换执行
|
## 已可运行:文件格式转换执行
|
||||||
|
|
||||||
- 源码:[CSV→JSON Worker 示例](../../sdk/go/examples/file-conversion/main.go),[完整说明](file-conversion.md)。
|
- 源码:[CSV→JSON Worker 示例](../../examples/file-conversion/main.go),[完整说明](file-conversion.md)。
|
||||||
- 前置:专用 Worker 凭据、已启动服务和当前迁移;标准隔离回归会自动运行本例。
|
- 前置:专用 Worker 凭据、已启动服务和当前迁移;标准隔离回归会自动运行本例。
|
||||||
- 输入与步骤:提交带稳定操作键的两行 CSV 快照 → Worker 注册 `file.convert.csv-json` v1 → 领取、授权并转换 → 查询 JSON 结果 → 排空。
|
- 输入与步骤:提交带稳定操作键的两行 CSV 快照 → Worker 注册 `file.convert.csv-json` v1 → 领取、授权并转换 → 查询 JSON 结果 → 排空。
|
||||||
- 预期:`SUCCEEDED`、一次尝试、`records.json`、`rows=2`,重复提交仍沿用原任务身份。
|
- 预期:`SUCCEEDED`、一次尝试、`records.json`、`rows=2`,重复提交仍沿用原任务身份。
|
||||||
@@ -59,7 +65,7 @@
|
|||||||
|
|
||||||
## 已可运行:文本规范化执行
|
## 已可运行:文本规范化执行
|
||||||
|
|
||||||
- 源码:[Worker 底层协议示例](../../sdk/go/examples/worker-protocol/main.go)。
|
- 源码:[Worker 底层协议示例](../../examples/worker-protocol/main.go)。
|
||||||
- 前置:专用 Worker 凭据,启动步骤见 [Worker SDK 文档](../sdk/worker.md)。
|
- 前置:专用 Worker 凭据,启动步骤见 [Worker SDK 文档](../sdk/worker.md)。
|
||||||
- 输入与步骤:提交 `{"text":" Hello Orbit "}` → 注册/容量 → 领取 → 开始授权 → 实际处理 → 确认结果 → 查询 → 排空。
|
- 输入与步骤:提交 `{"text":" Hello Orbit "}` → 注册/容量 → 领取 → 开始授权 → 实际处理 → 确认结果 → 查询 → 排空。
|
||||||
- 预期:SUCCEEDED、一次尝试、结果 `{"text":"HELLO ORBIT"}`。
|
- 预期:SUCCEEDED、一次尝试、结果 `{"text":"HELLO ORBIT"}`。
|
||||||
@@ -68,7 +74,7 @@
|
|||||||
|
|
||||||
## 已可运行:自动 Worker 并发消费
|
## 已可运行:自动 Worker 并发消费
|
||||||
|
|
||||||
- 源码:[自动 Worker 示例](../../sdk/go/examples/worker/main.go),[接入与启动命令](../sdk/worker-runtime.md)。
|
- 源码:[自动 Worker 示例](../../examples/worker/main.go),[接入与启动命令](../sdk/worker-runtime.md)。
|
||||||
- 步骤:注册版本化处理器 → Run → 以容量 2 消费三条文本任务 → 查询结果 → 取消 Run context 并等待排空。
|
- 步骤:注册版本化处理器 → Run → 以容量 2 消费三条文本任务 → 查询结果 → 取消 Run context 并等待排空。
|
||||||
- 预期:三条规范化结果、started=3、confirmed=3、running=0;输入与结果均由实际代码处理。
|
- 预期:三条规范化结果、started=3、confirmed=3、running=0;输入与结果均由实际代码处理。
|
||||||
- 异常:自动心跳/续租与原身份重连;普通错误/panic 按未知效果报告,超时退出不声称杀死 goroutine;完整故障证据见 [回归记录](../reports/phase-2-worker-runtime.md)。
|
- 异常:自动心跳/续租与原身份重连;普通错误/panic 按未知效果报告,超时退出不声称杀死 goroutine;完整故障证据见 [回归记录](../reports/phase-2-worker-runtime.md)。
|
||||||
@@ -76,7 +82,7 @@
|
|||||||
|
|
||||||
## 已可运行:执行历史与冲突证据核对
|
## 已可运行:执行历史与冲突证据核对
|
||||||
|
|
||||||
- 源码:[执行历史示例](../../sdk/go/examples/history/main.go),[启动命令和输入输出](../sdk/README.md#6-查询执行历史与证据)。
|
- 源码:[执行历史示例](../../examples/history/main.go),[启动命令和输入输出](../sdk/README.md#6-查询执行历史与证据)。
|
||||||
- 前置:迁移到当前版本、启动服务,专用凭据具备提交/读取与 Worker 权限。
|
- 前置:迁移到当前版本、启动服务,专用凭据具备提交/读取与 Worker 权限。
|
||||||
- 步骤:创建模拟任务 → 开始授权 → 保存 v1 结果 → 用另一消息身份报告 v2 → 查询尝试/证据列表与详情 → 核对任务仍保留 v1。
|
- 步骤:创建模拟任务 → 开始授权 → 保存 v1 结果 → 用另一消息身份报告 v2 → 查询尝试/证据列表与详情 → 核对任务仍保留 v1。
|
||||||
- 预期:SUCCEEDED、attempts=1、evidence=1、reason=RESULT_CONFLICT、disposition=EVIDENCE_ONLY;实际结果中的大整数保持精确。
|
- 预期:SUCCEEDED、attempts=1、evidence=1、reason=RESULT_CONFLICT、disposition=EVIDENCE_ONLY;实际结果中的大整数保持精确。
|
||||||
@@ -85,7 +91,7 @@
|
|||||||
|
|
||||||
## 已可运行:有限安全重试
|
## 已可运行:有限安全重试
|
||||||
|
|
||||||
- 源码:[重试示例](../../sdk/go/examples/retry/main.go),[完整启动命令与结果](../api/retries.md#5-运行完整示例)。
|
- 源码:[重试示例](../../examples/retry/main.go),[完整启动命令与结果](../api/retries.md#5-运行完整示例)。
|
||||||
- 前置:当前迁移及服务,具有提交、读取和 Worker 权限的专用凭据。
|
- 前置:当前迁移及服务,具有提交、读取和 Worker 权限的专用凭据。
|
||||||
- 步骤:第一次在产生效果前模拟明确拒绝 → 平台持久化退避 → 第二次处理文本 → 查询两次尝试 → 排空。
|
- 步骤:第一次在产生效果前模拟明确拒绝 → 平台持久化退避 → 第二次处理文本 → 查询两次尝试 → 排空。
|
||||||
- 预期:SUCCEEDED、attempts=2、first_retryable=true、结果 `{"text":"SAFE RETRY"}`;Worker started=2、confirmed=2、running=0。
|
- 预期:SUCCEEDED、attempts=2、first_retryable=true、结果 `{"text":"SAFE RETRY"}`;Worker started=2、confirmed=2、running=0。
|
||||||
@@ -94,7 +100,7 @@
|
|||||||
|
|
||||||
## 已可运行:周期对象快照
|
## 已可运行:周期对象快照
|
||||||
|
|
||||||
- 源码:[调度示例](../../sdk/go/examples/schedules/main.go),[初始化、调用与输出](../api/schedules.md#6-sdk-与完整示例)。
|
- 源码:[调度示例](../../examples/schedules/main.go),[初始化、调用与输出](../api/schedules.md#6-sdk-与完整示例)。
|
||||||
- 前置:当前迁移、服务,显式 --schedule-access 与 --worker-access 的专用开发凭据。
|
- 前置:当前迁移、服务,显式 --schedule-access 与 --worker-access 的专用开发凭据。
|
||||||
- 步骤:创建暂停的有限每秒计划 → 恢复 → 自动 Worker 返回输入快照 → 查到至少两次任务成功 → 暂停并排空。
|
- 步骤:创建暂停的有限每秒计划 → 恢复 → 自动 Worker 返回输入快照 → 查到至少两次任务成功 → 暂停并排空。
|
||||||
- 预期:不同 occurrence/task 身份、相同计划版本、paused=true、running=0;精确整数保留。
|
- 预期:不同 occurrence/task 身份、相同计划版本、paused=true、running=0;精确整数保留。
|
||||||
@@ -103,7 +109,7 @@
|
|||||||
|
|
||||||
## 已可运行:运行中任务取消
|
## 已可运行:运行中任务取消
|
||||||
|
|
||||||
- 源码:[取消示例](../../sdk/go/examples/cancellation/main.go),[初始化、调用与输出](../api/cancellation.md)。
|
- 源码:[取消示例](../../examples/cancellation/main.go),[初始化、调用与输出](../api/cancellation.md)。
|
||||||
- 前置:当前迁移、全部服务节点升级完成,凭据具备任务提交/读取/取消和 Worker 权限。
|
- 前置:当前迁移、全部服务节点升级完成,凭据具备任务提交/读取/取消和 Worker 权限。
|
||||||
- 步骤:提交并开始受控任务 → 保存取消意图 → 处理器遵守 context 停止 → 明确确认本次无未结清效果 → 查询最终状态并排空。
|
- 步骤:提交并开始受控任务 → 保存取消意图 → 处理器遵守 context 停止 → 明确确认本次无未结清效果 → 查询最终状态并排空。
|
||||||
- 预期:accepted_state=RUNNING、final_state=CANCELLED、attempts=1、running=0。
|
- 预期:accepted_state=RUNNING、final_state=CANCELLED、attempts=1、running=0。
|
||||||
@@ -112,7 +118,7 @@
|
|||||||
|
|
||||||
## 已可运行:队列限速与积压背压
|
## 已可运行:队列限速与积压背压
|
||||||
|
|
||||||
- 源码:[资源控制示例](../../sdk/go/examples/resources/main.go),[初始化、字段与完整输出](../api/queues.md)。
|
- 源码:[资源控制示例](../../examples/resources/main.go),[初始化、字段与完整输出](../api/queues.md)。
|
||||||
- 前置:当前迁移与服务,凭据具备全部队列的 queues:manage、任务及 Worker 权限。
|
- 前置:当前迁移与服务,凭据具备全部队列的 queues:manage、任务及 Worker 权限。
|
||||||
- 步骤:创建并暂停积压上限为 2 的队列 → 第三个任务被 429 拒绝 → 满额下核对第一条原回执 → 取消释放空间 → 用被拒原身份再提交 → 恢复并按每秒 1 次开始执行。
|
- 步骤:创建并暂停积压上限为 2 的队列 → 第三个任务被 429 拒绝 → 满额下核对第一条原回执 → 取消释放空间 → 用被拒原身份再提交 → 恢复并按每秒 1 次开始执行。
|
||||||
- 预期:QUEUE_FULL、duplicate=true、两个任务 SUCCEEDED/attempts=1、unfinished=0。
|
- 预期:QUEUE_FULL、duplicate=true、两个任务 SUCCEEDED/attempts=1、unfinished=0。
|
||||||
@@ -121,7 +127,7 @@
|
|||||||
|
|
||||||
## 已可运行:跨命名空间共享配额
|
## 已可运行:跨命名空间共享配额
|
||||||
|
|
||||||
- 源码:[应用资源示例](../../sdk/go/examples/application-resources/main.go),[双命名空间初始化、字段和输出](../api/application-resources.md)。
|
- 源码:[应用资源示例](../../examples/application-resources/main.go),[双命名空间初始化、字段和输出](../api/application-resources.md)。
|
||||||
- 前置:同一独立、空闲示例应用下的两个命名空间,凭据显式具备应用资源管理权限;不需要 Worker。
|
- 前置:同一独立、空闲示例应用下的两个命名空间,凭据显式具备应用资源管理权限;不需要 Worker。
|
||||||
- 步骤:应用上限设为 1 → first 受理 → second 全局满额 → 取消 first → second 以原受理键成功 → 取消收尾并按 CAS 恢复配置。
|
- 步骤:应用上限设为 1 → first 受理 → second 全局满额 → 取消 first → second 以原受理键成功 → 取消收尾并按 CAS 恢复配置。
|
||||||
- 预期:APPLICATION_FULL/429、same_identity_retry_accepted=true、三个用量归零。
|
- 预期:APPLICATION_FULL/429、same_identity_retry_accepted=true、三个用量归零。
|
||||||
@@ -130,9 +136,9 @@
|
|||||||
|
|
||||||
## 计划场景与验收标准
|
## 计划场景与验收标准
|
||||||
|
|
||||||
已可运行的 [优先级场景](../api/priority-and-fairness.md#4-sdk-完整场景) 提供 [完整源码](../../sdk/go/examples/priority/main.go):按 0、9、4 受理后,以单槽 Worker 按 9、4、0 完成近同时到期的任务。前置为当前迁移/服务与队列、任务及 Worker 权限;预期 completed=3、attempts_per_task=1、worker_drained=true。无外部业务副作用,历史保留,失败非零退出,任务自带总时限;长等待补偿和其他应用隔离由真实数据库回归验证。
|
已可运行的 [优先级场景](../api/priority-and-fairness.md#4-sdk-完整场景) 提供 [完整源码](../../examples/priority/main.go):按 0、9、4 受理后,以单槽 Worker 按 9、4、0 完成近同时到期的任务。前置为当前迁移/服务与队列、任务及 Worker 权限;预期 completed=3、attempts_per_task=1、worker_drained=true。无外部业务副作用,历史保留,失败非零退出,任务自带总时限;长等待补偿和其他应用隔离由真实数据库回归验证。
|
||||||
|
|
||||||
已可运行的 [跨队列资源互斥场景](../api/keyed-concurrency.md#5-完整可运行场景) 提供 [完整源码](../../sdk/go/examples/keyed-concurrency/main.go):两个队列的自动 Worker 操作同一资源,核对递增序号,并模拟资源端拒绝旧写入。输入使用显式 concurrency_key;预期 completed=2、resource_writes=2、stale_write_rejected=true。示例仅使用受控内存资源,无外部副作用;正常排空、保留历史,任务自带总时限。内存 mutex 不是生产分布式存储,生产必须在资源端原子持久化序号、操作身份和实际更新。
|
已可运行的 [跨队列资源互斥场景](../api/keyed-concurrency.md#5-完整可运行场景) 提供 [完整源码](../../examples/keyed-concurrency/main.go):两个队列的自动 Worker 操作同一资源,核对递增序号,并模拟资源端拒绝旧写入。输入使用显式 concurrency_key;预期 completed=2、resource_writes=2、stale_write_rejected=true。示例仅使用受控内存资源,无外部副作用;正常排空、保留历史,任务自带总时限。内存 mutex 不是生产分布式存储,生产必须在资源端原子持久化序号、操作身份和实际更新。
|
||||||
|
|
||||||
| 场景 | 输入与步骤 | 预期结果 | 必测异常 | 状态 |
|
| 场景 | 输入与步骤 | 预期结果 | 必测异常 | 状态 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
@@ -147,7 +153,7 @@
|
|||||||
|
|
||||||
## 已可运行:订阅管理与幂等回执
|
## 已可运行:订阅管理与幂等回执
|
||||||
|
|
||||||
- 源码:[订阅管理示例](../../sdk/go/examples/subscriptions/main.go),[启动、输入输出和异常说明](subscriptions.md)。
|
- 源码:[订阅管理示例](../../examples/subscriptions/main.go),[启动、输入输出和异常说明](subscriptions.md)。
|
||||||
- 前置:当前迁移及服务,具备 events:manage/read/consume 的专用开发凭据;初始化显式 --event-access。
|
- 前置:当前迁移及服务,具备 events:manage/read/consume 的专用开发凭据;初始化显式 --event-access。
|
||||||
- 步骤:创建 → 同键核对 → 暂停 → 恢复 → 查询与分页 → 最终暂停。
|
- 步骤:创建 → 同键核对 → 暂停 → 恢复 → 查询与分页 → 最终暂停。
|
||||||
- 预期:duplicate=true、resumed_revision=3、final_enabled=false、revision=4、pending=0。
|
- 预期:duplicate=true、resumed_revision=3、final_enabled=false、revision=4、pending=0。
|
||||||
@@ -156,7 +162,7 @@
|
|||||||
|
|
||||||
## 已可运行:结果消费协议与跨连接确认
|
## 已可运行:结果消费协议与跨连接确认
|
||||||
|
|
||||||
- 源码:[事件协议示例](../../sdk/go/examples/event-protocol/main.go),[启动与输出说明](event-protocol.md)。
|
- 源码:[事件协议示例](../../examples/event-protocol/main.go),[启动与输出说明](event-protocol.md)。
|
||||||
- 操作:创建订阅 → 提交并取消任务 → 接收冻结结果 → ACK → 重连重报原证明 → 核对原确认时间 → 暂停订阅。
|
- 操作:创建订阅 → 提交并取消任务 → 接收冻结结果 → ACK → 重连重报原证明 → 核对原确认时间 → 暂停订阅。
|
||||||
- 输出:ack_replayed=true、same_receipt=true、pending=0、subscription_enabled=false。
|
- 输出:ack_replayed=true、same_receipt=true、pending=0、subscription_enabled=false。
|
||||||
- 范围:本例只做读取验证,不写业务数据;[自动消费器](../sdk/event-consumer.md) 的生产接入须提供事务Inbox,不能把示例当作业务恰好一次保证。
|
- 范围:本例只做读取验证,不写业务数据;[自动消费器](../sdk/event-consumer.md) 的生产接入须提供事务Inbox,不能把示例当作业务恰好一次保证。
|
||||||
@@ -171,7 +177,7 @@
|
|||||||
|
|
||||||
## 已可运行:批量受理与部分补交
|
## 已可运行:批量受理与部分补交
|
||||||
|
|
||||||
- 源码:[batch](../../sdk/go/examples/batch/main.go),[完整启动命令和输出](batch.md)。
|
- 源码:[batch](../../examples/batch/main.go),[完整启动命令和输出](batch.md)。
|
||||||
- 步骤:独立暂停队列 → 三项批量提交、两项成功一项满额 → 整批核对原回执 → 取消释放额度 → 仅原身份补交第三项 → 取消收尾。
|
- 步骤:独立暂停队列 → 三项批量提交、两项成功一项满额 → 整批核对原回执 → 取消释放额度 → 仅原身份补交第三项 → 取消收尾。
|
||||||
- 预期:accepted=2、replayed=2、retried_items=1、unfinished=0。
|
- 预期:accepted=2、replayed=2、retried_items=1、unfinished=0。
|
||||||
- 异常:逐项区分REJECTED/UNKNOWN/NOT_ATTEMPTED;真实HTTP响应丢失后恢复无重复,保留当前权限和额度控制。
|
- 异常:逐项区分REJECTED/UNKNOWN/NOT_ATTEMPTED;真实HTTP响应丢失后恢复无重复,保留当前权限和额度控制。
|
||||||
@@ -179,7 +185,7 @@
|
|||||||
|
|
||||||
## 已可运行:独立任务人工核查与关联重试
|
## 已可运行:独立任务人工核查与关联重试
|
||||||
|
|
||||||
- 源码:[recovery](../../sdk/go/examples/recovery/main.go),[完整运行与失败说明](recovery.md)。
|
- 源码:[recovery](../../examples/recovery/main.go),[完整运行与失败说明](recovery.md)。
|
||||||
- 步骤:模拟UNKNOWN → 人工声明历史执行结束 → 核查FAILED → 原键核对 → 创建retry_of关联新任务 → 执行成功 → 查询原任务及操作审计。
|
- 步骤:模拟UNKNOWN → 人工声明历史执行结束 → 核查FAILED → 原键核对 → 创建retry_of关联新任务 → 执行成功 → 查询原任务及操作审计。
|
||||||
- 预期:原任务FAILED、新任务SUCCEEDED,原尝试和审计保留;管理凭据显式--recovery-access。
|
- 预期:原任务FAILED、新任务SUCCEEDED,原尝试和审计保留;管理凭据显式--recovery-access。
|
||||||
- 异常:真实HTTP在核查/重试提交后丢响应,原身份恢复同一审计和新任务;迟到成功只进入证据。
|
- 异常:真实HTTP在核查/重试提交后丢响应,原身份恢复同一审计和新任务;迟到成功只进入证据。
|
||||||
@@ -187,7 +193,7 @@
|
|||||||
|
|
||||||
## 已可运行:业务键和创建时间查询
|
## 已可运行:业务键和创建时间查询
|
||||||
|
|
||||||
- 源码:[task-query](../../sdk/go/examples/task-query/main.go),[完整启动与异常说明](task-query.md)。
|
- 源码:[task-query](../../examples/task-query/main.go),[完整启动与异常说明](task-query.md)。
|
||||||
- 步骤:提交三个模拟任务 → 精确业务键及时间范围过滤 → 每页一条遍历 → 验证上界排除 → 取消收尾。
|
- 步骤:提交三个模拟任务 → 精确业务键及时间范围过滤 → 每页一条遍历 → 验证上界排除 → 取消收尾。
|
||||||
- 预期:matched=2、pages=2、before_excluded=true、cancelled=3。
|
- 预期:matched=2、pages=2、before_excluded=true、cancelled=3。
|
||||||
- 范围:普通提交/读取/取消权限;不调用外部系统、不启动Worker,所有历史保留,状态分页不是数据库快照。
|
- 范围:普通提交/读取/取消权限;不调用外部系统、不启动Worker,所有历史保留,状态分页不是数据库快照。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 批量文件作业受理与部分补交
|
# 批量文件作业受理与部分补交
|
||||||
|
|
||||||
[完整源码](../../sdk/go/examples/batch/main.go) 使用独立Go SDK,不导入服务端模型或数据库驱动。它演示受理与取消,不启动Worker或转换真实文件。
|
[完整源码](../../examples/batch/main.go) 使用独立Go SDK,不导入服务端模型或数据库驱动。它演示受理与取消,不启动Worker或转换真实文件。
|
||||||
|
|
||||||
## 前置与运行
|
## 前置与运行
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ go run ./cmd/orbit
|
|||||||
另开终端:
|
另开终端:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/batch.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/batch.credentials.json \
|
||||||
go run ./examples/batch
|
go run ./batch
|
||||||
```
|
```
|
||||||
|
|
||||||
也接受ORBIT_TOKEN,环境值优先。示例自行创建并暂停一个独立队列,未终结上限为2,不改变已有队列或应用配置。前置应用剩余额度至少2。
|
也接受ORBIT_TOKEN,环境值优先。示例自行创建并暂停一个独立队列,未终结上限为2,不改变已有队列或应用配置。前置应用剩余额度至少2。
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# 相对延迟与原请求恢复
|
# 相对延迟与原请求恢复
|
||||||
|
|
||||||
源码:[delayed-task/main.go](../../sdk/go/examples/delayed-task/main.go)。示例提交等待1500毫秒的对象任务,启动自动Worker,核对实际执行历史,再在任务完成后重报原请求。处理器原样返回示例JSON,无外部副作用。
|
源码:[delayed-task/main.go](../../examples/delayed-task/main.go)。示例提交等待1500毫秒的对象任务,启动自动Worker,核对实际执行历史,再在任务完成后重报原请求。处理器原样返回示例JSON,无外部副作用。
|
||||||
|
|
||||||
## 准备与运行
|
## 准备与运行
|
||||||
|
|
||||||
先完成[迁移和Worker凭据初始化](../sdk/worker.md#1-可运行的文本规范化示例),启动当前Orbit。凭据需要任务提交、读取及Worker连接/消费权限,队列须有可用额度。仓库根目录执行:
|
先完成[迁移和Worker凭据初始化](../sdk/worker.md#1-可运行的文本规范化示例),启动当前Orbit。凭据需要任务提交、读取及Worker连接/消费权限,队列须有可用额度。仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/worker.credentials.json \
|
||||||
go run ./examples/delayed-task
|
go run ./delayed-task
|
||||||
```
|
```
|
||||||
|
|
||||||
也可用ORBIT_TOKEN和ORBIT_QUEUE_ID覆盖凭据文件中的对应值。示例总上下文20秒、任务总时限15秒、单次执行5秒、最多1次尝试;每次运行使用独立操作及处理器身份,不会消费其他处理器的任务。
|
也可用ORBIT_TOKEN和ORBIT_QUEUE_ID覆盖凭据文件中的对应值。示例总上下文20秒、任务总时限15秒、单次执行5秒、最多1次尝试;每次运行使用独立操作及处理器身份,不会消费其他处理器的任务。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 结果消费协议示例
|
# 结果消费协议示例
|
||||||
|
|
||||||
[完整代码](../../sdk/go/examples/event-protocol/main.go) 使用正式 Go SDK 创建取消结果订阅,提交并取消一个待执行任务,接收并验证冻结结果,然后 ACK;关闭连接后在新连接中重报原证明,验证原确认时间和去重回执,最后暂停本次订阅。
|
[完整代码](../../examples/event-protocol/main.go) 使用正式 Go SDK 创建取消结果订阅,提交并取消一个待执行任务,接收并验证冻结结果,然后 ACK;关闭连接后在新连接中重报原证明,验证原确认时间和去重回执,最后暂停本次订阅。
|
||||||
|
|
||||||
本例只读取并验证结果,没有业务数据库写入。生产业务应使用 [自动消费器及事务去重约定](../sdk/event-consumer.md)。持久化接入见 [独立 MySQL Inbox 示例](result-inbox.md)。
|
本例只读取并验证结果,没有业务数据库写入。生产业务应使用 [自动消费器及事务去重约定](../sdk/event-consumer.md)。持久化接入见 [独立 MySQL Inbox 示例](result-inbox.md)。
|
||||||
|
|
||||||
@@ -8,12 +8,13 @@
|
|||||||
|
|
||||||
先按 [订阅管理示例](subscriptions.md) 建立隔离示例队列和带 --event-access 的凭据,启动 Orbit。凭据需要 tasks:submit、tasks:cancel、events:manage/read/consume 及该队列范围;订阅绑定创建凭据,不能临时换凭据消费。
|
先按 [订阅管理示例](subscriptions.md) 建立隔离示例队列和带 --event-access 的凭据,启动 Orbit。凭据需要 tasks:submit、tasks:cancel、events:manage/read/consume 及该队列范围;订阅绑定创建凭据,不能临时换凭据消费。
|
||||||
|
|
||||||
从 sdk/go 目录运行:
|
从仓库根目录进入 examples 后运行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/event.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/event.credentials.json \
|
||||||
go run ./examples/event-protocol
|
go run ./event-protocol
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以传 ORBIT_TOKEN 与 ORBIT_QUEUE_ID,环境变量覆盖凭据文件对应值。每次运行新建随机订阅及任务身份,全部流程最多 15 秒。本例是短流程,两个连接均短于一次心跳周期;长期运行必须用自动消费器或主动 Ping。
|
也可以传 ORBIT_TOKEN 与 ORBIT_QUEUE_ID,环境变量覆盖凭据文件对应值。每次运行新建随机订阅及任务身份,全部流程最多 15 秒。本例是短流程,两个连接均短于一次心跳周期;长期运行必须用自动消费器或主动 Ping。
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
需要当前迁移、已启动 Orbit 服务,以及具备任务提交/读取和 Worker 消费权限的专用凭据。凭据文件应只保存在本地受保护目录:
|
需要当前迁移、已启动 Orbit 服务,以及具备任务提交/读取和 Worker 消费权限的专用凭据。凭据文件应只保存在本地受保护目录:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/worker.credentials.json \
|
||||||
go run ./examples/file-conversion
|
go run ./file-conversion
|
||||||
```
|
```
|
||||||
|
|
||||||
## 调用链与预期
|
## 调用链与预期
|
||||||
@@ -50,8 +50,8 @@ input=records.csv output=records.json rows=2 state=SUCCEEDED worker_drained=true
|
|||||||
单元转换校验:
|
单元转换校验:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
GOWORK=off go test ./examples/file-conversion
|
GOWORK=off go test ./file-conversion
|
||||||
```
|
```
|
||||||
|
|
||||||
`make integration` 会在一次性 MySQL 和真实 Orbit HTTP 服务上运行该示例,确认提交、Worker 执行、结果查询和排空均通过。该场景不证明外部对象存储的持久性,也不替代生产容量、故障切换或 RPO/RTO 验收。
|
`make integration` 会在一次性 MySQL 和真实 Orbit HTTP 服务上运行该示例,确认提交、Worker 执行、结果查询和排空均通过。该场景不证明外部对象存储的持久性,也不替代生产容量、故障切换或 RPO/RTO 验收。
|
||||||
|
|||||||
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。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 未知结果的核查与人工重试示例
|
# 未知结果的核查与人工重试示例
|
||||||
|
|
||||||
完整源码:[SDK recovery/main.go](../../sdk/go/examples/recovery/main.go)。只处理本地模拟对象,不调用任何真实外部业务系统。接口细节见[人工恢复手册](../api/task-recovery.md)。
|
完整源码:[SDK recovery/main.go](../../examples/recovery/main.go)。只处理本地模拟对象,不调用任何真实外部业务系统。接口细节见[人工恢复手册](../api/task-recovery.md)。
|
||||||
|
|
||||||
## 准备与运行
|
## 准备与运行
|
||||||
|
|
||||||
@@ -20,10 +20,10 @@ go run ./cmd/orbit
|
|||||||
另开终端,从仓库根目录执行:
|
另开终端,从仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/recovery.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/recovery.credentials.json \
|
||||||
go run ./examples/recovery
|
go run ./recovery
|
||||||
```
|
```
|
||||||
|
|
||||||
演示凭据为了完整运行同时包含Worker与人工管理权限;生产将它们分开。凭据文件0600、token不输出;也接受ORBIT_TOKEN/ORBIT_QUEUE_ID,环境值优先。
|
演示凭据为了完整运行同时包含Worker与人工管理权限;生产将它们分开。凭据文件0600、token不输出;也接受ORBIT_TOKEN/ORBIT_QUEUE_ID,环境值优先。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 订阅管理与幂等回执示例
|
# 订阅管理与幂等回执示例
|
||||||
|
|
||||||
状态:已提供 [完整可运行源码](../../sdk/go/examples/subscriptions/main.go)。演示 HTTP 管理 SDK;完整结果消费、重连及持久化 Inbox 示例继续开发。协议与字段见 [订阅接口](../api/subscriptions.md)。
|
状态:已提供 [完整可运行源码](../../examples/subscriptions/main.go)。演示 HTTP 管理 SDK;完整结果消费、重连及持久化 Inbox 示例继续开发。协议与字段见 [订阅接口](../api/subscriptions.md)。
|
||||||
|
|
||||||
## 前置和运行
|
## 前置和运行
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ go run ./cmd/orbit
|
|||||||
另一个终端从仓库根目录执行:
|
另一个终端从仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/subscriptions.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/subscriptions.credentials.json \
|
||||||
go run ./examples/subscriptions
|
go run ./subscriptions
|
||||||
```
|
```
|
||||||
|
|
||||||
也可设置 ORBIT_TOKEN 和 ORBIT_QUEUE_ID,环境值优先于凭据文件。生产接入使用 HTTPS;不要提交凭据文件或在命令行打印秘密。
|
也可设置 ORBIT_TOKEN 和 ORBIT_QUEUE_ID,环境值优先于凭据文件。生产接入使用 HTTPS;不要提交凭据文件或在命令行打印秘密。
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 1. 数据与状态
|
## 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接入接口
|
## 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
|
```go
|
||||||
func Open(ctx context.Context, dsn string) (*sql.DB, error)
|
func Open(ctx context.Context, dsn string) (*sql.DB, error)
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# 业务键与时间范围查询示例
|
# 业务键与时间范围查询示例
|
||||||
|
|
||||||
源码:[task-query/main.go](../../sdk/go/examples/task-query/main.go)。本例提交三个模拟对象任务,不启动Worker,演示精确业务键、创建时间区间和任务摘要分页。
|
源码:[task-query/main.go](../../examples/task-query/main.go)。本例提交三个模拟对象任务,不启动Worker,演示精确业务键、创建时间区间和任务摘要分页。
|
||||||
|
|
||||||
## 准备与运行
|
## 准备与运行
|
||||||
|
|
||||||
按[SDK初始化步骤](../sdk/README.md)迁移数据库、初始化专用开发队列及普通提交/读取/取消凭据,启动当前Orbit服务。从仓库根目录执行:
|
按[SDK初始化步骤](../sdk/README.md)迁移数据库、初始化专用开发队列及普通提交/读取/取消凭据,启动当前Orbit服务。从仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/development.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/development.credentials.json \
|
||||||
go run ./examples/task-query
|
go run ./task-query
|
||||||
```
|
```
|
||||||
|
|
||||||
也可使用ORBIT_TOKEN与ORBIT_QUEUE_ID,环境值优先。无需额外管理权限,凭据秘密不输出。队列应有至少三个未终结任务空位;每次运行的处理器与业务键带独立标识,避免已有任务干扰。
|
也可使用ORBIT_TOKEN与ORBIT_QUEUE_ID,环境值优先。无需额外管理权限,凭据秘密不输出。队列应有至少三个未终结任务空位;每次运行的处理器与业务键带独立标识,避免已有任务干扰。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本例运行真实HTTP发布与读取,验证六种节点的格式以及不可变版本。当前不创建流程实例、不执行外部业务。实际运行另见[并行作业与回执示例](workflow.md)。
|
本例运行真实HTTP发布与读取,验证六种节点的格式以及不可变版本。当前不创建流程实例、不执行外部业务。实际运行另见[并行作业与回执示例](workflow.md)。
|
||||||
|
|
||||||
源码:[workflow-definition/main.go](../../sdk/go/examples/workflow-definition/main.go)。接口:[定义发布](../api/workflow-definitions.md);字段规范:[结构版本1](../api/workflow-definition-format.md)。
|
源码:[workflow-definition/main.go](../../examples/workflow-definition/main.go)。接口:[定义发布](../api/workflow-definitions.md);字段规范:[结构版本1](../api/workflow-definition-format.md)。
|
||||||
|
|
||||||
## 1. 准备与运行
|
## 1. 准备与运行
|
||||||
|
|
||||||
@@ -15,11 +15,11 @@ bin/orbit-admin bootstrap --application definition-demo --namespace default --en
|
|||||||
首次创建文件后保留,不要覆盖或打印凭据内容。该标志只增加定义读取和发布动作;生产凭据应限制到需要的命名空间及队列。
|
首次创建文件后保留,不要覆盖或打印凭据内容。该标志只增加定义读取和发布动作;生产凭据应限制到需要的命名空间及队列。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=/tmp/orbit-definition.credentials.json \
|
ORBIT_CREDENTIALS_FILE=/tmp/orbit-definition.credentials.json \
|
||||||
ORBIT_OPERATION_ID=object-definition-demo-001 \
|
ORBIT_OPERATION_ID=object-definition-demo-001 \
|
||||||
go run ./examples/workflow-definition
|
go run ./workflow-definition
|
||||||
```
|
```
|
||||||
|
|
||||||
开发回环地址可用HTTP,生产使用HTTPS。可改用`ORBIT_TOKEN`及`ORBIT_QUEUE_ID`,不把Token写入URL或提交版本库。不传`ORBIT_OPERATION_ID`时随机生成并先输出;发生未知结果时保存此操作ID,保持相同队列及源码再次运行。已成功的相同操作ID也可以重复执行,旧版本与revision不会增加。
|
开发回环地址可用HTTP,生产使用HTTPS。可改用`ORBIT_TOKEN`及`ORBIT_QUEUE_ID`,不把Token写入URL或提交版本库。不传`ORBIT_OPERATION_ID`时随机生成并先输出;发生未知结果时保存此操作ID,保持相同队列及源码再次运行。已成功的相同操作ID也可以重复执行,旧版本与revision不会增加。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 有界轮询:请求重试与业务轮次分别计数
|
# 有界轮询:请求重试与业务轮次分别计数
|
||||||
|
|
||||||
[完整源码](../../sdk/go/examples/workflow-poll/main.go)通过公共SDK发布POLL定义,启动真实Worker,并请求一个本机HTTP渠道夹具。流程先由TASK向本机渠道提交一次作业,受理回执提供固定operation_id,随后POLL只查询这个作业;平台没有集成任何具体业务渠道。
|
[完整源码](../../examples/workflow-poll/main.go)通过公共SDK发布POLL定义,启动真实Worker,并请求一个本机HTTP渠道夹具。流程先由TASK向本机渠道提交一次作业,受理回执提供固定operation_id,随后POLL只查询这个作业;平台没有集成任何具体业务渠道。
|
||||||
|
|
||||||
同一命令依次运行两个流程:
|
同一命令依次运行两个流程:
|
||||||
|
|
||||||
@@ -27,10 +27,10 @@ go run ./cmd/orbit
|
|||||||
bootstrap只执行一次,凭据文件存在时不会覆盖。另开终端,从仓库根目录运行:
|
bootstrap只执行一次,凭据文件存在时不会覆盖。另开终端,从仓库根目录运行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/poll.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/poll.credentials.json \
|
||||||
go run ./examples/workflow-poll
|
go run ./workflow-poll
|
||||||
```
|
```
|
||||||
|
|
||||||
程序自行创建并关闭本机HTTP夹具,不需要启动第三方系统,不向公网提交操作。正式接入使用HTTPS/WSS和分角色凭据,将只读查询处理器替换为自己的渠道调用。
|
程序自行创建并关闭本机HTTP夹具,不需要启动第三方系统,不向公网提交操作。正式接入使用HTTPS/WSS和分角色凭据,将只读查询处理器替换为自己的渠道调用。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
场景:一个对象关联多次处理流程,管理端需要按固定定义版本、精确业务关联及时间范围查询等待中的实例,再进入详情处理。示例同时证明业务键末尾有无空格属于不同关联。
|
场景:一个对象关联多次处理流程,管理端需要按固定定义版本、精确业务关联及时间范围查询等待中的实例,再进入详情处理。示例同时证明业务键末尾有无空格属于不同关联。
|
||||||
|
|
||||||
源码:[workflow-query/main.go](../../sdk/go/examples/workflow-query/main.go)。服务端入口:[筛选接口](../api/workflow-queries.md)。示例发布一个SIGNAL等待流程,创建三个实例,其中两个使用相同业务键、第三个去掉尾部空格;每页1项读取匹配的两个实例,最后将三个实例取消并等待CANCELLED。
|
源码:[workflow-query/main.go](../../examples/workflow-query/main.go)。服务端入口:[筛选接口](../api/workflow-queries.md)。示例发布一个SIGNAL等待流程,创建三个实例,其中两个使用相同业务键、第三个去掉尾部空格;每页1项读取匹配的两个实例,最后将三个实例取消并等待CANCELLED。
|
||||||
|
|
||||||
## 启动
|
## 启动
|
||||||
|
|
||||||
@@ -22,10 +22,10 @@ go run ./cmd/orbit
|
|||||||
另开终端:
|
另开终端:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/workflow-query.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/workflow-query.credentials.json \
|
||||||
go run ./examples/workflow-query
|
go run ./workflow-query
|
||||||
```
|
```
|
||||||
|
|
||||||
查询本身只要求workflows:read。此完整示例额外需要研发创建/发布、启动与控制权限,由上述bootstrap选项授予。SIGNAL等待不用Worker,也不模拟外部业务成功;正常结束通过显式取消结清。
|
查询本身只要求workflows:read。此完整示例额外需要研发创建/发布、启动与控制权限,由上述bootstrap选项授予。SIGNAL等待不用Worker,也不模拟外部业务成功;正常结束通过显式取消结清。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 提交后回执丢失:核查、暂停与有证据恢复
|
# 提交后回执丢失:核查、暂停与有证据恢复
|
||||||
|
|
||||||
[完整源码](../../sdk/go/examples/workflow-recovery/main.go)使用真实Orbit、自动Worker和本机HTTP渠道夹具。夹具先保存外部提交结果,再直接关闭连接,确保Worker实际遇到传输错误。流程进入NEEDS_REVIEW后,程序在受控夹具中模拟一个已经核实事实的管理员,逐项提交恢复决定。
|
[完整源码](../../examples/workflow-recovery/main.go)使用真实Orbit、自动Worker和本机HTTP渠道夹具。夹具先保存外部提交结果,再直接关闭连接,确保Worker实际遇到传输错误。流程进入NEEDS_REVIEW后,程序在受控夹具中模拟一个已经核实事实的管理员,逐项提交恢复决定。
|
||||||
|
|
||||||
这是一份可运行的SDK接入与故障示例。夹具数据仅在本次进程内保存;真实系统需要持久证据和真实授权,不能把下面的演示核查逻辑接成任意业务的自动放行。
|
这是一份可运行的SDK接入与故障示例。夹具数据仅在本次进程内保存;真实系统需要持久证据和真实授权,不能把下面的演示核查逻辑接成任意业务的自动放行。
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ go run ./cmd/orbit
|
|||||||
初始化一次;原凭据文件不会被覆盖。另开终端,从仓库根目录运行:
|
初始化一次;原凭据文件不会被覆盖。另开终端,从仓库根目录运行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/workflow-recovery.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/workflow-recovery.credentials.json \
|
||||||
go run ./examples/workflow-recovery
|
go run ./workflow-recovery
|
||||||
```
|
```
|
||||||
|
|
||||||
开发凭据包含定义、启动、Worker、控制、恢复及审计权限,便于单命令演示。正式接入应使用HTTPS/WSS,并为业务Worker与管理员分别签发所需权限;Worker执行权不自动授予恢复权。
|
开发凭据包含定义、启动、Worker、控制、恢复及审计权限,便于单命令演示。正式接入应使用HTTPS/WSS,并为业务Worker与管理员分别签发所需权限;Worker执行权不自动授予恢复权。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 并行生成结果 → 等待外部回执 → 后续任务
|
# 并行生成结果 → 等待外部回执 → 后续任务
|
||||||
|
|
||||||
[完整源码](../../sdk/go/examples/workflow/main.go)通过独立Go SDK发布固定版本、启动实例,并使用真实自动Worker处理任务。流程是两个文本规范化任务并行完成并汇合,等待500毫秒,等待模拟外部确认信号,最后用左侧输出执行发布步骤。输入为 `{"text":" hello orbit "}`,输出为 `{"text":"HELLO ORBIT"}`。
|
[完整源码](../../examples/workflow/main.go)通过独立Go SDK发布固定版本、启动实例,并使用真实自动Worker处理任务。流程是两个文本规范化任务并行完成并汇合,等待500毫秒,等待模拟外部确认信号,最后用左侧输出执行发布步骤。输入为 `{"text":" hello orbit "}`,输出为 `{"text":"HELLO ORBIT"}`。
|
||||||
|
|
||||||
## 1. 启动
|
## 1. 启动
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ go run ./cmd/orbit
|
|||||||
初始化仅执行一次,文件存在时不会覆盖。重用该凭据运行例子无需重复bootstrap。另开终端,从仓库根目录执行:
|
初始化仅执行一次,文件存在时不会覆盖。重用该凭据运行例子无需重复bootstrap。另开终端,从仓库根目录执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd sdk/go
|
cd examples
|
||||||
ORBIT_URL=http://127.0.0.1:8080 \
|
ORBIT_URL=http://127.0.0.1:8080 \
|
||||||
ORBIT_CREDENTIALS_FILE=../../.local/workflow.credentials.json \
|
ORBIT_CREDENTIALS_FILE=../.local/workflow.credentials.json \
|
||||||
go run ./examples/workflow
|
go run ./workflow
|
||||||
```
|
```
|
||||||
|
|
||||||
生产使用HTTPS/WSS,并为定义发布者、启动者、信号发送者及Worker分别签发所需权限。本示例使用一个专用开发凭据,未申请人工恢复或控制权限。
|
生产使用HTTPS/WSS,并为定义发布者、启动者、信号发送者及Worker分别签发所需权限。本示例使用一个专用开发凭据,未申请人工恢复或控制权限。
|
||||||
|
|||||||
@@ -6,14 +6,15 @@
|
|||||||
|
|
||||||
| 项目 | 证据/命令 | 状态 |
|
| 项目 | 证据/命令 | 状态 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 服务端与 SDK 代码来自同一提交 | `git rev-parse HEAD`;发布时记录不可变提交 | 待发布候选冻结 |
|
| 服务端与 SDK 代码来自同一提交 | 解引用 `v0.1.0` 与 `sdk/go/v0.1.0` | 已固定同一提交,空缓存外部项目取包/编译/校验和通过,见[远端发布报告](reports/phase-7-sdk-remote-release.md);生产发布审批仍待完成 |
|
||||||
| Go 工具链 | Go 1.27.1;Docker `golang:1.27.1`,真实 `go.mod`/`GOWORK=off` | 模块级 race/vet/build、MySQL 存储/进程组、24 个 SDK 示例及两个独立业务模块真实集成已通过;发布环境仍需复核 |
|
| Go 工具链 | Go 1.27.1;Docker `golang:1.27.1`,真实 `go.mod`/`GOWORK=off` | 模块级 race/vet/build、MySQL 存储/进程组、24 个 SDK 示例及两个独立业务模块真实集成已通过;发布环境仍需复核 |
|
||||||
| 容器发布物 | `make image`;[容器发布说明](21-container-deployment.md) | native linux/arm64 与显式 linux/amd64 镜像构建已通过,支持注入版本身份并由 `/version` 核对;生产镜像标签、摘要、签名和目标编排环境仍需冻结 |
|
| 容器发布物 | `make image`;[容器发布说明](21-container-deployment.md) | native linux/arm64 与显式 linux/amd64 镜像构建已通过,支持注入版本身份并由 `/version` 核对;生产镜像标签、摘要、签名和目标编排环境仍需冻结 |
|
||||||
| 发布物身份清单 | `orbit-admin release manifest`、`orbit-admin verify --manifest`;[运维接口](api/operations.md) | 已实现无需数据库的机器可读清单及部署前二进制绑定校验,包含构建身份、协议版本和全部迁移校验和;正式发布仍需把清单、镜像摘要与目标库 `verify` 输出一并冻结 |
|
| 发布物身份清单 | `orbit-admin release manifest`、`orbit-admin verify --manifest`;[运维接口](api/operations.md) | 已实现无需数据库的机器可读清单及部署前二进制绑定校验,包含构建身份、协议版本和全部迁移校验和;正式发布仍需把清单、镜像摘要与目标库 `verify` 输出一并冻结 |
|
||||||
| 服务端单元、竞态、vet、build | `make test && make vet && make build` | Go 1.27.1 Docker 等价命令已通过;宿主机 `make` 因未安装 Go 不能作为本机证据 |
|
| 服务端单元、竞态、vet、build | `make test && make vet && make build` | Go 1.27.1 Docker 及宿主机本地门禁已通过,见[当前回归状态](reports/phase-7-regression-status.md) |
|
||||||
| 当前协议线一致 | `make protocol-check` | 已通过:HTTP `/v1`、Worker/Event v1、流程 schema 1 |
|
| 当前协议线一致 | `make protocol-check` | 已通过:HTTP `/v1`、Worker/Event v1、流程 schema 1 |
|
||||||
| SDK 独立性和发布门禁 | `make sdk-check` | Go 1.27.1 Docker 下独立 module、示例和业务模块 race/vet/build 已通过 |
|
| SDK 独立性和发布门禁 | `make sdk-check` | Go 1.27.1 Docker 下独立 module、示例和业务模块 race/vet/build 已通过 |
|
||||||
| 文档与示例 | 本清单、[接口手册](api/README.md)、[SDK手册](sdk/README.md)、[场景示例](examples/README.md) | 文档链接和 JSON 块已检查;宿主机全量端到端见[当前回归状态](reports/phase-7-regression-status.md) |
|
| 文档与示例 | 本清单、[接口手册](api/README.md)、[SDK手册](sdk/README.md)、[场景示例](examples/README.md) | 文档链接和 JSON 块已检查;宿主机全量端到端见[当前回归状态](reports/phase-7-regression-status.md) |
|
||||||
|
| 四项闭环验收 | [P7 四项验收记录](reports/phase-7-acceptance-20260921.md) | 当前 Docker/MySQL 环境已通过流程恢复、指标告警、故障容量和 SDK 接入闭环;目标生产拓扑仍需单独验收 |
|
||||||
|
|
||||||
## 数据库与安全
|
## 数据库与安全
|
||||||
|
|
||||||
@@ -34,17 +35,17 @@
|
|||||||
| 派发未送达、提交前后断线、最终信号竞争 | [故障边界报告](reports/phase-6-fault-boundaries.md)、[当前回归状态](reports/phase-7-regression-status.md) | Go 1.27.1 已通过 MySQL 存储、关键 SDK/Orbit 进程故障组、24 个示例串跑和两个独立业务模块集成;跨版本矩阵和目标拓扑仍待发布环境 |
|
| 派发未送达、提交前后断线、最终信号竞争 | [故障边界报告](reports/phase-6-fault-boundaries.md)、[当前回归状态](reports/phase-7-regression-status.md) | Go 1.27.1 已通过 MySQL 存储、关键 SDK/Orbit 进程故障组、24 个示例串跑和两个独立业务模块集成;跨版本矩阵和目标拓扑仍待发布环境 |
|
||||||
| 备份恢复 | `scripts/test-backup-restore.sh`、[恢复报告](reports/phase-6-backup-restore.md) | Go 1.27.1 构建的当前二进制已通过隔离恢复;生产介质待验收 |
|
| 备份恢复 | `scripts/test-backup-restore.sh`、[恢复报告](reports/phase-6-backup-restore.md) | Go 1.27.1 构建的当前二进制已通过隔离恢复;生产介质待验收 |
|
||||||
| MySQL 短暂不可用恢复 | `scripts/test-db-recovery.sh`、[恢复报告](reports/phase-6-db-recovery.md) | Go 1.27.1 构建的当前二进制已通过单节点同卷演练;主从/跨故障域待验收 |
|
| MySQL 短暂不可用恢复 | `scripts/test-db-recovery.sh`、[恢复报告](reports/phase-6-db-recovery.md) | Go 1.27.1 构建的当前二进制已通过单节点同卷演练;主从/跨故障域待验收 |
|
||||||
| 运行观测和只读快照 | `/metrics`、`orbit-admin observe snapshot`、[观测手册](api/observability.md) | 固定基数和有界单节点快照已实现 |
|
| 运行观测和只读快照 | `/metrics`、`orbit-admin observe snapshot`、[观测手册](api/observability.md) | 固定基数、有界单节点快照、8 条告警规则及离线规则测试已通过;生产采集网络和通知仍待目标环境 |
|
||||||
| 告警、值守、回滚/前向修复 | 目标监控、值班和发布工单 | 待目标环境 |
|
| 告警、值守、回滚/前向修复 | 目标监控、值班和发布工单 | 待目标环境 |
|
||||||
|
|
||||||
## 容量与生产高可用
|
## 容量与生产高可用
|
||||||
|
|
||||||
| 项目 | 必须提供的证据 | 状态 |
|
| 项目 | 必须提供的证据 | 状态 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 受理、派发、流程推进和恢复吞吐 | 固定硬件、数据量、任务/流程混合模型,p50/p95/p99和资源曲线 | 已有受理样本、[真实Worker短/混合/中长执行样本](reports/phase-6-execution-load.md)、[Weave双分支流程样本](reports/phase-6-workflow-load.md)及[定时集中触发/积压曲线样本](reports/phase-6-backlog.md);目标硬件资源曲线、生产容量和HA仍未验收 |
|
| 受理、派发、流程推进和恢复吞吐 | 固定硬件、数据量、任务/流程混合模型,p50/p95/p99和资源曲线 | 已有受理样本、[真实Worker短/混合/中长执行样本](reports/phase-6-execution-load.md)、[Weave双分支流程样本](reports/phase-6-workflow-load.md)、[定时集中触发/积压曲线样本](reports/phase-6-backlog.md)及本轮[四项闭环验收](reports/phase-7-acceptance-20260921.md);目标硬件资源曲线、生产容量和HA仍未验收 |
|
||||||
| 100万+企业规模推导 | [容量模型](19-capacity-model.md):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 |
|
| 100万+企业规模推导 | [容量模型](19-capacity-model.md):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 |
|
||||||
| MySQL 复制、切换、RPO/RTO | [隔离 GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md);目标拓扑上的实际切换、复制确认、恢复点和恢复时间 | 隔离源库/副本复制、手工提升和原幂等键重放已通过;自动切换、目标拓扑、RPO/RTO仍未验收 |
|
| MySQL 复制、切换、RPO/RTO | [隔离 GTID 复制与手工提升演练](reports/phase-6-mysql-replication.md);目标拓扑上的实际切换、复制确认、恢复点和恢复时间 | 隔离源库/副本复制、手工提升和原幂等键重放已通过;自动切换、目标拓扑、RPO/RTO仍未验收 |
|
||||||
| 慢消费者、重连风暴、磁盘和锁等待 | 受控故障负载及积压清空证据 | SDK 受控连续断线和真实 HTTP/WebSocket 容量1慢消费者已回归(见[重连报告](reports/phase-6-reconnect-storm.md)、[慢消费者报告](reports/phase-6-slow-consumer.md));跨节点风暴、磁盘和锁等待未验收 |
|
| 慢消费者、重连风暴、磁盘和锁等待 | `make stability-check`,受控故障负载及积压清空证据 | SDK 受控连续断线、跨节点重连风暴、真实 HTTP/WebSocket 容量1慢消费者、真实 MySQL 锁等待/死锁和保留期磁盘增长已加入定向回归;目标生产拓扑的规模和告警阈值仍待验收 |
|
||||||
| 跨版本 SDK/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 历史提交 `a50d5fa` 的稳定任务 API 双向 smoke 已通过(见[报告](reports/phase-7-historical-compatibility.md));正式标签和全协议矩阵仍未验收 |
|
| 跨版本 SDK/服务端 | 至少一个正式旧标签与候选版本的双向矩阵 | 历史提交 `a50d5fa` 的稳定任务 API 双向 smoke 已通过(见[报告](reports/phase-7-historical-compatibility.md));正式标签和全协议矩阵仍未验收 |
|
||||||
|
|
||||||
发布候选只有在所有“待目标环境/未验收”项得到对应环境证据后,才能进入生产审批。当前代码和阶段回归不等同于生产高可用、容量、RPO/RTO或百万企业承诺。
|
发布候选只有在所有“待目标环境/未验收”项得到对应环境证据后,才能进入生产审批。当前代码和阶段回归不等同于生产高可用、容量、RPO/RTO或百万企业承诺。
|
||||||
|
|||||||
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专用集成不在本批交付内。
|
||||||
40
docs/reports/phase-10-ingress.md
Normal file
40
docs/reports/phase-10-ingress.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# HTTPS/WSS 入口基线验证
|
||||||
|
|
||||||
|
验证日期:2026-09-22
|
||||||
|
验证范围:Orbit 主机端口边界、Nginx HTTPS 配置语法、WebSocket Upgrade 路由、健康探针和控制台登录限速配置。
|
||||||
|
验证环境:Docker Desktop、Nginx 1.27 Alpine、当前 Compose 配置。
|
||||||
|
|
||||||
|
## 已通过
|
||||||
|
|
||||||
|
### Orbit 端口边界
|
||||||
|
|
||||||
|
使用临时数据库变量展开 Compose 配置,确认默认映射为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
published: "8080"
|
||||||
|
target: 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Orbit 的明文业务端口默认只对本机开放,公网入口由反向代理承担。需要直接暴露端口的隔离环境必须显式设置 `ORBIT_HTTP_BIND`,不能把开发配置直接当作生产入口。
|
||||||
|
|
||||||
|
### Nginx 配置语法
|
||||||
|
|
||||||
|
`deploy/nginx/orbit.conf.example` 使用一次性自签名证书挂载到 Nginx 1.27 Alpine 容器中执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
|
||||||
|
nginx: configuration file /etc/nginx/nginx.conf test is successful
|
||||||
|
```
|
||||||
|
|
||||||
|
配置包含 HTTP 到 HTTPS 跳转、TLS 1.2/1.3、`/v1/worker/connect` 和 `/v1/events/connect` 的 Upgrade/Connection 头、独立长连接超时、`/health/live`、`/health/ready` 代理以及 Console 登录来源限速。metrics 不经过业务入口。
|
||||||
|
|
||||||
|
## 尚未由本报告关闭的事项
|
||||||
|
|
||||||
|
本报告只验证配置和端口边界,没有使用真实域名、正式证书、外部负载均衡或公网流量。生产仍需验证证书签发/轮换、DNS、WSS 长连接重连、代理节点故障、真实健康摘除和入口访问日志脱敏;这些不能由一次 `nginx -t` 推导出来。
|
||||||
42
docs/reports/phase-11-two-node-compose.md
Normal file
42
docs/reports/phase-11-two-node-compose.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 两节点 Compose 覆盖验证
|
||||||
|
|
||||||
|
验证日期:2026-09-22
|
||||||
|
验证范围:两个 Orbit 节点共享 MySQL、独立节点就绪、停止单节点后的对等节点可用、停止节点重新加入。
|
||||||
|
验证环境:Docker Desktop、MySQL 8.4、当前 `orbit:dev` 镜像、Go 1.27.1 构建产物。
|
||||||
|
|
||||||
|
## 验证入口
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose \
|
||||||
|
-f deploy/docker-compose.yml \
|
||||||
|
-f deploy/docker-compose.ha.yml \
|
||||||
|
up -d mysql verify orbit-a orbit-b
|
||||||
|
```
|
||||||
|
|
||||||
|
本机已有大量 Docker 自定义网络,本次只在临时测试覆盖文件中使用默认 `bridge` 网络和 MySQL 链接,以避开本机地址池耗尽;正式的 `deploy/docker-compose.ha.yml` 保持 Compose 项目隔离网络。
|
||||||
|
|
||||||
|
## 结果
|
||||||
|
|
||||||
|
一次性 MySQL 空库完成健康检查、迁移、权限收紧和 schema 核验后,两个 Orbit 节点均启动并通过就绪探针:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ha-e2e=both_ready
|
||||||
|
```
|
||||||
|
|
||||||
|
停止 `orbit-a` 后,`orbit-b` 继续返回就绪:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ha-e2e=peer_ready_after_orbit_a_stop
|
||||||
|
```
|
||||||
|
|
||||||
|
重新拉起 `orbit-a`,它重新完成迁移/核验依赖并恢复就绪:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ha-e2e=orbit_a_rejoined
|
||||||
|
```
|
||||||
|
|
||||||
|
两节点分别绑定临时主机端口 `18087` 和 `18088`;正式入口由 Nginx upstream 或负载均衡器同时指向两个节点,并以 `/health/ready` 作为摘流条件。
|
||||||
|
|
||||||
|
## 验收边界
|
||||||
|
|
||||||
|
该回归证明节点无本地权威状态、停止/重加入顺序和 Compose 依赖正确;它没有证明数据库自动切换、跨故障域、负载均衡器故障、生产证书轮换、容量余量或 RPO/RTO。任务执行和租约接管仍需在目标拓扑中按真实 Worker、慢消费者和数据库故障场景留证。
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
- 受理、计划生成、领取、Start、取消、Finish、安全重试及到期恢复共同维护应用计数,跨命名空间与队列共享。空轮询与失败受理不消耗计数,重复回执不重复计费。
|
- 受理、计划生成、领取、Start、取消、Finish、安全重试及到期恢复共同维护应用计数,跨命名空间与队列共享。空轮询与失败受理不消耗计数,重复回执不重复计费。
|
||||||
- 额度行与认证身份分离,统一按应用额度 → 队列 → 任务/尝试 → 许可顺序取锁;后台扫描跳过忙碌额度,应用停用后仍可释放到期尝试。
|
- 额度行与认证身份分离,统一按应用额度 → 队列 → 任务/尝试 → 许可顺序取锁;后台扫描跳过忙碌额度,应用停用后仍可释放到期尝试。
|
||||||
- 管理 CLI 支持 `--application-id` 在已有启用应用下增加命名空间;应用管理权限必须显式 `--resource-access`,普通队列及 Worker 凭据不扩权。
|
- 管理 CLI 支持 `--application-id` 在已有启用应用下增加命名空间;应用管理权限必须显式 `--resource-access`,普通队列及 Worker 凭据不扩权。
|
||||||
- [双命名空间 SDK 示例](../../sdk/go/examples/application-resources/main.go):第一侧受理,第二侧 APPLICATION_FULL,第一侧取消释放,第二侧原身份重报成功,取消收尾并按 CAS 恢复配置。
|
- [双命名空间 SDK 示例](../../examples/application-resources/main.go):第一侧受理,第二侧 APPLICATION_FULL,第一侧取消释放,第二侧原身份重报成功,取消收尾并按 CAS 恢复配置。
|
||||||
|
|
||||||
## 2. 环境和实际回归
|
## 2. 环境和实际回归
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
- [服务契约](../../pulse/service/batch.go):最多32项,逐项独立事务与幂等,ACCEPTED/REJECTED/UNKNOWN/NOT_ATTEMPTED四类结果。未知数据库结果或凭据失效后停止余项,上下文结束保留已确认前项。
|
- [服务契约](../../pulse/service/batch.go):最多32项,逐项独立事务与幂等,ACCEPTED/REJECTED/UNKNOWN/NOT_ATTEMPTED四类结果。未知数据库结果或凭据失效后停止余项,上下文结束保留已确认前项。
|
||||||
- [HTTP入口](../../internal/transport/httpserver/tasks.go):`POST /v1/tasks:batch`,全体正文严格解析后才受理,每项正文幂等键,总请求512 KiB。完整响应使用HTTP200,逐项标记结果。
|
- [HTTP入口](../../internal/transport/httpserver/tasks.go):`POST /v1/tasks:batch`,全体正文严格解析后才受理,每项正文幂等键,总请求512 KiB。完整响应使用HTTP200,逐项标记结果。
|
||||||
- [独立SDK](../../sdk/go/batch.go):SubmitTasks及中文字段说明,不自动生成身份、拆批或重试;校验返回数量、顺序、状态与回执归属。
|
- [独立SDK](../../sdk/go/batch.go):SubmitTasks及中文字段说明,不自动生成身份、拆批或重试;校验返回数量、顺序、状态与回执归属。
|
||||||
- [运行示例](../../sdk/go/examples/batch/main.go):两项受理、一项满额,原键核对后释放配额,仅补交被拒项并取消收尾。
|
- [运行示例](../../examples/batch/main.go):两项受理、一项满额,原键核对后释放配额,仅补交被拒项并取消收尾。
|
||||||
- [接口手册](../api/batch-tasks.md)、[场景说明](../examples/batch.md)、[开发计划](../08-development-plan.md) 同步维护。
|
- [接口手册](../api/batch-tasks.md)、[场景说明](../examples/batch.md)、[开发计划](../08-development-plan.md) 同步维护。
|
||||||
|
|
||||||
不增加数据库模型或迁移,平台仍为35个实体、289个中文注释持久化字段;不增加外键、级联或SDK数据库依赖。
|
不增加数据库模型或迁移,平台仍为35个实体、289个中文注释持久化字段;不增加外键、级联或SDK数据库依赖。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- 取消后不延长期限、不产生后续重试;有效成功结果仍能确认,普通取消错误或失联保持未知效果。
|
- 取消后不延长期限、不产生后续重试;有效成功结果仍能确认,普通取消错误或失联保持未知效果。
|
||||||
- 明确取消结果使用 task.finish / CANCELLED;本次确认停止不能清除此前未核实的外部效果。
|
- 明确取消结果使用 task.finish / CANCELLED;本次确认停止不能清除此前未核实的外部效果。
|
||||||
- 可选择的 task.cancel 通知、Resume/Start/Renew 中的当前取消意图、独立 SDK 自动停止处理器和有界收尾。
|
- 可选择的 task.cancel 通知、Resume/Start/Renew 中的当前取消意图、独立 SDK 自动停止处理器和有界收尾。
|
||||||
- [完整取消场景](../../sdk/go/examples/cancellation/main.go)、[接口与升级说明](../api/cancellation.md) 已同步交付。
|
- [完整取消场景](../../examples/cancellation/main.go)、[接口与升级说明](../api/cancellation.md) 已同步交付。
|
||||||
- 迁移 000007 扩展取消证据;000008 移除 70 个历史外键,保留索引、唯一约束、CHECK、触发器和数据。实体数仍为 32,没有新增持久化字段。
|
- 迁移 000007 扩展取消证据;000008 移除 70 个历史外键,保留索引、唯一约束、CHECK、触发器和数据。实体数仍为 32,没有新增持久化字段。
|
||||||
- 开发规范明确禁止再引入外键。应用启动/readiness 核验迁移及零外键要求;任务、计划及 Worker 关联由服务端事务校验。
|
- 开发规范明确禁止再引入外键。应用启动/readiness 核验迁移及零外键要求;任务、计划及 Worker 关联由服务端事务校验。
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
- [EventConsumer](../../sdk/go/event_consumer.go):自动心跳、指数退避重连、本地1–32个在途事件、成功后确认、ACK不明时原证明核对、处理器失败不确认、取消与有界收尾。
|
- [EventConsumer](../../sdk/go/event_consumer.go):自动心跳、指数退避重连、本地1–32个在途事件、成功后确认、ACK不明时原证明核对、处理器失败不确认、取消与有界收尾。
|
||||||
- [类型及秘密隐藏](../../sdk/go/event_types.go):ResultEvent不给业务处理器租约秘密;Grant/Proof仍可显式序列化用于受保护的存储,普通fmt隐藏敏感数据。
|
- [类型及秘密隐藏](../../sdk/go/event_types.go):ResultEvent不给业务处理器租约秘密;Grant/Proof仍可显式序列化用于受保护的存储,普通fmt隐藏敏感数据。
|
||||||
- [严格解码](../../sdk/go/event_codec.go):大小写、重复键、方向、会话、规范uint64、证明编码和事件/交付/快照关联。大整数不经float64转换。协议类WebSocket关闭按永久错误处理。
|
- [严格解码](../../sdk/go/event_codec.go):大小写、重复键、方向、会话、规范uint64、证明编码和事件/交付/快照关联。大整数不经float64转换。协议类WebSocket关闭按永久错误处理。
|
||||||
- [协议例子](../../sdk/go/examples/event-protocol/main.go):产生取消结果、读取、ACK、重连重报、验证原确认时间、暂停订阅。它只读取结果,不执行接收方业务写入。
|
- [协议例子](../../examples/event-protocol/main.go):产生取消结果、读取、ACK、重连重报、验证原确认时间、暂停订阅。它只读取结果,不执行接收方业务写入。
|
||||||
- SDK保持独立模块,无新增数据库驱动、服务端包或持久化模型。本阶段不增加迁移、外键或级联删除。
|
- SDK保持独立模块,无新增数据库驱动、服务端包或持久化模型。本阶段不增加迁移、外键或级联删除。
|
||||||
|
|
||||||
## 2. 验证环境与入口
|
## 2. 验证环境与入口
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- 续租、取消、结果、退避及到期恢复同步维护两类许可。UNKNOWN 释放执行许可并保留核查与未知效果,旧结果不能释放后续尝试的许可。
|
- 续租、取消、结果、退避及到期恢复同步维护两类许可。UNKNOWN 释放执行许可并保留核查与未知效果,旧结果不能释放后续尝试的许可。
|
||||||
- Worker 显式协商 concurrency_keys;旧 Worker 不消费有键任务,自动 SDK 默认开启。协议 Schema、SDK 精确 uint64、尝试历史与计划模板同步支持。
|
- Worker 显式协商 concurrency_keys;旧 Worker 不消费有键任务,自动 SDK 默认开启。协议 Schema、SDK 精确 uint64、尝试历史与计划模板同步支持。
|
||||||
- [000011 迁移](../../internal/storage/mysql/migrations/000011_keyed_concurrency.up.sql) 新增四个中文注释字段,无外键。当前 33 个实体、273 个直接声明的持久化字段均有中文注释,SQL 同步 COMMENT。
|
- [000011 迁移](../../internal/storage/mysql/migrations/000011_keyed_concurrency.up.sql) 新增四个中文注释字段,无外键。当前 33 个实体、273 个直接声明的持久化字段均有中文注释,SQL 同步 COMMENT。
|
||||||
- [完整示例](../../sdk/go/examples/keyed-concurrency/main.go) 创建两个队列、两个 Worker、相同资源键,核对序号并模拟资源端拒绝旧写入。
|
- [完整示例](../../examples/keyed-concurrency/main.go) 创建两个队列、两个 Worker、相同资源键,核对序号并模拟资源端拒绝旧写入。
|
||||||
|
|
||||||
## 2. 环境与命令
|
## 2. 环境与命令
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- 以最多 270 秒的排序优势实现优先级加速;等待超过此优势的旧任务不被不断到来的最高级新任务持续插队。没有承诺 270 秒内完成或严格 FIFO。
|
- 以最多 270 秒的排序优势实现优先级加速;等待超过此优势的旧任务不被不断到来的最高级新任务持续插队。没有承诺 270 秒内完成或严格 FIFO。
|
||||||
- [节点领取池](../../internal/dispatch/pool.go) 将原来的各连接并发轮询改为应用 → 队列 → 连接请求轮转。总并发 4、单应用在途 1、待处理加在途最多 128;每连接最多一个未完成请求。应用归属由认证后的数据库关系取得,不能按自报应用或不同命名空间分组。
|
- [节点领取池](../../internal/dispatch/pool.go) 将原来的各连接并发轮询改为应用 → 队列 → 连接请求轮转。总并发 4、单应用在途 1、待处理加在途最多 128;每连接最多一个未完成请求。应用归属由认证后的数据库关系取得,不能按自报应用或不同命名空间分组。
|
||||||
- 同命名空间/队列/精确能力的空探测在节点内合并一个周期,缓存最多 128 项;控制收发继续运行。所有任务状态、许可、预算和执行授权仍由数据库事务决定。
|
- 同命名空间/队列/精确能力的空探测在节点内合并一个周期,缓存最多 128 项;控制收发继续运行。所有任务状态、许可、预算和执行授权仍由数据库事务决定。
|
||||||
- [可运行示例](../../sdk/go/examples/priority/main.go)、协议 Schema、接口与 SDK 文档同步更新。当前仍为 33 个实体,274 个直接声明持久化字段全部有中文注释,最终数据库外键为零。
|
- [可运行示例](../../examples/priority/main.go)、协议 Schema、接口与 SDK 文档同步更新。当前仍为 33 个实体,274 个直接声明持久化字段全部有中文注释,最终数据库外键为零。
|
||||||
|
|
||||||
## 2. 环境和执行
|
## 2. 环境和执行
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 计划补跑受相同积压上限限制,保留未受理的原触发点;释放空间后按冻结漏跑策略接续。
|
- 计划补跑受相同积压上限限制,保留未受理的原触发点;释放空间后按冻结漏跑策略接续。
|
||||||
- 迁移 000009 增加四个队列字段并回填既有未终结任务,Go/SQL 均有中文说明,无数据库外键。
|
- 迁移 000009 增加四个队列字段并回填既有未终结任务,Go/SQL 均有中文说明,无数据库外键。
|
||||||
- 源码核对 257 个直接声明的持久化字段均有中文注释;实体仍为 32 个。
|
- 源码核对 257 个直接声明的持久化字段均有中文注释;实体仍为 32 个。
|
||||||
- [队列接口与完整 SDK 场景](../api/queues.md)、[resources 示例](../../sdk/go/examples/resources/main.go) 同步交付。
|
- [队列接口与完整 SDK 场景](../api/queues.md)、[resources 示例](../../examples/resources/main.go) 同步交付。
|
||||||
|
|
||||||
## 2. 环境及执行结果
|
## 2. 环境及执行结果
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- 每节点 128 条事件连接;异步领取池 4 个全局在途、同应用 1 个在途、等待加在途总量 128,按认证应用及订阅轮转。控制收发不被同连接的慢领取阻塞。连接容量仍由持久化租约校验。
|
- 每节点 128 条事件连接;异步领取池 4 个全局在途、同应用 1 个在途、等待加在途总量 128,按认证应用及订阅轮转。控制收发不被同连接的慢领取阻塞。连接容量仍由持久化租约校验。
|
||||||
- [严格解码](../../internal/transport/httpserver/event_codec.go) 与 [公开 Schema](../../protocol/eventsv1/events.schema.json):方向、必填、字段精确大小写、规范 uint64、证明格式和消息大小;JSON 重复键、伪造响应字段或错误会话被拒绝。
|
- [严格解码](../../internal/transport/httpserver/event_codec.go) 与 [公开 Schema](../../protocol/eventsv1/events.schema.json):方向、必填、字段精确大小写、规范 uint64、证明格式和消息大小;JSON 重复键、伪造响应字段或错误会话被拒绝。
|
||||||
- [管理 SDK](../../sdk/go/subscriptions.go) 与 [独立 DTO](../../sdk/go/subscription_types.go):七个管理/查询方法及类型,保留大整数和显式布尔控制,不依赖服务端或数据库包。
|
- [管理 SDK](../../sdk/go/subscriptions.go) 与 [独立 DTO](../../sdk/go/subscription_types.go):七个管理/查询方法及类型,保留大整数和显式布尔控制,不依赖服务端或数据库包。
|
||||||
- [完整管理例子](../../sdk/go/examples/subscriptions/main.go) 已纳入统一隔离环境脚本。原始 WSS 测试辅助代码不作为正式消费 SDK 对外提供。
|
- [完整管理例子](../../examples/subscriptions/main.go) 已纳入统一隔离环境脚本。原始 WSS 测试辅助代码不作为正式消费 SDK 对外提供。
|
||||||
|
|
||||||
## 2. 验证环境与结果
|
## 2. 验证环境与结果
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- 抖动退避写入数据库 available_at;与尝试结果、许可释放、事件、结果审计及操作回执原子提交。后台回收保留含节点身份与决策的事件。
|
- 抖动退避写入数据库 available_at;与尝试结果、许可释放、事件、结果审计及操作回执原子提交。后台回收保留含节点身份与决策的事件。
|
||||||
- 结果换消息身份重报也取原已应用回执;过滤早到的证据回执,避免借用后续尝试状态。
|
- 结果换消息身份重报也取原已应用回执;过滤早到的证据回执,避免借用后续尝试状态。
|
||||||
- 前向迁移 000005 增加三个中文注释字段和结果回执索引;实体仍为 32 个,旧迁移未改动。
|
- 前向迁移 000005 增加三个中文注释字段和结果回执索引;实体仍为 32 个,旧迁移未改动。
|
||||||
- 同步独立 SDK、Worker Schema、查询字段、[接口手册](../api/retries.md) 与 [完整示例](../../sdk/go/examples/retry/main.go)。
|
- 同步独立 SDK、Worker Schema、查询字段、[接口手册](../api/retries.md) 与 [完整示例](../../examples/retry/main.go)。
|
||||||
|
|
||||||
## 2. 环境与执行命令
|
## 2. 环境与执行命令
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- SKIP / COALESCE / CATCH_UP 漏跑策略与 PARALLEL / SERIAL / SKIP 重叠策略;明确补偿窗口、延迟容差、批量和扫描间隔。
|
- SKIP / COALESCE / CATCH_UP 漏跑策略与 PARALLEL / SERIAL / SKIP 重叠策略;明确补偿窗口、延迟容差、批量和扫描间隔。
|
||||||
- 发布新版本保留旧生效边界前的积压;跳过和合并范围不越过版本边界。已产生任务保留当时模板。
|
- 发布新版本保留旧生效边界前的积压;跳过和合并范围不越过版本边界。已产生任务保留当时模板。
|
||||||
- 多节点用数据库锁与唯一触发身份竞争;触发决定、对应任务、事件及进度一起提交。
|
- 多节点用数据库锁与唯一触发身份竞争;触发决定、对应任务、事件及进度一起提交。
|
||||||
- 实际 orbit 命令持续扫描,独立 SDK 与 [完整周期快照示例](../../sdk/go/examples/schedules/main.go) 同步交付。
|
- 实际 orbit 命令持续扫描,独立 SDK 与 [完整周期快照示例](../../examples/schedules/main.go) 同步交付。
|
||||||
- 迁移 000006 增加运行字段与索引,实体仍为 32 个;新模型字段及 SQL COMMENT 均为中文;源码核对 32 个持久化模型、253 个直接声明的持久化字段均有中文说明。
|
- 迁移 000006 增加运行字段与索引,实体仍为 32 个;新模型字段及 SQL COMMENT 均为中文;源码核对 32 个持久化模型、253 个直接声明的持久化字段均有中文说明。
|
||||||
|
|
||||||
完整字段、权限、状态与示例见 [接口手册](../api/schedules.md)。
|
完整字段、权限、状态与示例见 [接口手册](../api/schedules.md)。
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
- [独立模块](../../examples/task-outbox/README.md)只依赖独立Orbit SDK与业务侧MySQL驱动,核心SDK没有新增依赖。
|
- [独立模块](../../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/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/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。
|
- [CLI](../../examples/task-outbox/cmd/sender/main.go)及[完整命令/方法/恢复手册](../examples/task-outbox.md):init、enqueue、send、status和demo。
|
||||||
- [Makefile](../../Makefile)和[隔离脚本](../../scripts/test-mysql.sh)纳入独立模块,使用单独业务数据库和专用平台凭据,不触碰现有开发数据库。
|
- [Makefile](../../Makefile)和[隔离脚本](../../scripts/test-mysql.sh)纳入独立模块,使用单独业务数据库和专用平台凭据,不触碰现有开发数据库。
|
||||||
|
|
||||||
## 2. 定向数据库与故障证据
|
## 2. 定向数据库与故障证据
|
||||||
|
|
||||||
[数据库回归](../../examples/task-outbox/internal/outbox/store_integration_test.go)验证:
|
[数据库回归](../../adapters/outbox/mysql/store_integration_test.go)验证:
|
||||||
|
|
||||||
1. 8个相同业务操作并发入库只有一条业务记录/Outbox,7次重复核对;修改正文、源引用或目标队列不能复用原操作。
|
1. 8个相同业务操作并发入库只有一条业务记录/Outbox,7次重复核对;修改正文、源引用或目标队列不能复用原操作。
|
||||||
2. 实际业务INSERT失败时Outbox一起回滚;业务关联UPDATE失败时ACCEPTED一起回滚,恢复后可保存原回执。
|
2. 实际业务INSERT失败时Outbox一起回滚;业务关联UPDATE失败时ACCEPTED一起回滚,恢复后可保存原回执。
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
4. 错误目标、损坏文档、权限拒绝、幂等冲突和无效回执停止自动发送;背压遵守Retry-After,次数耗尽和截止产生REVIEW,最后一次未知响应不能绕过上限。
|
4. 错误目标、损坏文档、权限拒绝、幂等冲突和无效回执停止自动发送;背压遵守Retry-After,次数耗尽和截止产生REVIEW,最后一次未知响应不能绕过上限。
|
||||||
5. 实际Schema查询确认22个中文列注释、外键0,通过实际违规写入验证JSON、状态、次数和截止CHECK生效,领取查询使用ix_sender_due且无filesort;大整数9007199254740993在冻结请求与真实回执中保持精确。
|
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. 发现与修正
|
## 3. 发现与修正
|
||||||
|
|
||||||
@@ -73,3 +73,5 @@ make integration
|
|||||||
- REVIEW保留原数据,由接入方带证据和审计的管理流程处置;本示例未实现通用业务审核系统或跨业务ORM适配。
|
- REVIEW保留原数据,由接入方带证据和审计的管理流程处置;本示例未实现通用业务审核系统或跨业务ORM适配。
|
||||||
- 核心SDK、平台Schema和执行协议均未改变;新增业务示例不是已发布的兼容性SDK包。
|
- 核心SDK、平台Schema和执行协议均未改变;新增业务示例不是已发布的兼容性SDK包。
|
||||||
- 流程启动与信号Outbox类型需随Weave API实现;P6继续容量、数据库切换、备份恢复及运维验证。当前正确性与进程恢复证据不等同于生产高可用或百万规模容量结论。
|
- 流程启动与信号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)。
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
- [查询存储](../../internal/storage/mysql/task_query.go):固定四种索引分支、参数化SQL、创建时间/ID游标分页,v2摘要绑定全部筛选,旧游标只兼容原队列/状态查询。
|
- [查询存储](../../internal/storage/mysql/task_query.go):固定四种索引分支、参数化SQL、创建时间/ID游标分页,v2摘要绑定全部筛选,旧游标只兼容原队列/状态查询。
|
||||||
- [HTTP入口](../../internal/transport/httpserver/tasks.go)与[独立SDK](../../sdk/go/task_query.go):URL编码、UTC规范化、显式空参数与未知参数拒绝,SDK无数据库依赖。
|
- [HTTP入口](../../internal/transport/httpserver/tasks.go)与[独立SDK](../../sdk/go/task_query.go):URL编码、UTC规范化、显式空参数与未知参数拒绝,SDK无数据库依赖。
|
||||||
- [迁移000016](../../internal/storage/mysql/migrations/000016_task_query_filters.up.sql):business_key使用精确NO PAD二进制排序,增加两种队列业务键索引,中文字段含义同步。无新增模型字段、无外键/级联,仍为35个模型、289个持久化字段。
|
- [迁移000016](../../internal/storage/mysql/migrations/000016_task_query_filters.up.sql):business_key使用精确NO PAD二进制排序,增加两种队列业务键索引,中文字段含义同步。无新增模型字段、无外键/级联,仍为35个模型、289个持久化字段。
|
||||||
- [接口手册](../api/task-queries.md)、[完整示例说明](../examples/task-query.md)及[源码](../../sdk/go/examples/task-query/main.go)。
|
- [接口手册](../api/task-queries.md)、[完整示例说明](../examples/task-query.md)及[源码](../../examples/task-query/main.go)。
|
||||||
|
|
||||||
## 2. 真实数据库验证
|
## 2. 真实数据库验证
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
- [服务契约](../../pulse/service/recovery.go):显式历史执行结束/安全重试声明、原因、证据、版本及预算校验。
|
- [服务契约](../../pulse/service/recovery.go):显式历史执行结束/安全重试声明、原因、证据、版本及预算校验。
|
||||||
- [数据库事务](../../internal/storage/mysql/task_recovery.go):NEEDS_REVIEW核查为SUCCEEDED/FAILED,原尝试保留;独立FAILED/CANCELLED任务生成retry_of关联后继,原任务不改写。
|
- [数据库事务](../../internal/storage/mysql/task_recovery.go):NEEDS_REVIEW核查为SUCCEEDED/FAILED,原尝试保留;独立FAILED/CANCELLED任务生成retry_of关联后继,原任务不改写。
|
||||||
- [审计查询](../../internal/storage/mysql/task_operations.go):专门权限与父资源范围,摘要游标分页、详情原因和证据,真实凭据身份。
|
- [审计查询](../../internal/storage/mysql/task_operations.go):专门权限与父资源范围,摘要游标分页、详情原因和证据,真实凭据身份。
|
||||||
- [Go SDK](../../sdk/go/recovery.go)、[接口手册](../api/task-recovery.md)、[完整运行场景](../examples/recovery.md)及[源码](../../sdk/go/examples/recovery/main.go)。
|
- [Go SDK](../../sdk/go/recovery.go)、[接口手册](../api/task-recovery.md)、[完整运行场景](../examples/recovery.md)及[源码](../../examples/recovery/main.go)。
|
||||||
- [迁移000015](../../internal/storage/mysql/migrations/000015_task_manual_recovery.up.sql):现有RetryOf字段中文语义、同父后继唯一索引与禁止自身关联CHECK。35个模型、289个持久化字段,无新增字段,无外键/级联。
|
- [迁移000015](../../internal/storage/mysql/migrations/000015_task_manual_recovery.up.sql):现有RetryOf字段中文语义、同父后继唯一索引与禁止自身关联CHECK。35个模型、289个持久化字段,无新增字段,无外键/级联。
|
||||||
|
|
||||||
## 2. 已覆盖的数据库与协议场景
|
## 2. 已覆盖的数据库与协议场景
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
- [服务模型](../../pulse/service/source.go)与[存储查询](../../internal/storage/mysql/task_source.go):DIRECT、MANUAL_RETRY、SCHEDULE三类来源;计划ID、触发记录、冻结版本及合并范围;人工重试仅一跳父任务。
|
- [服务模型](../../pulse/service/source.go)与[存储查询](../../internal/storage/mysql/task_source.go):DIRECT、MANUAL_RETRY、SCHEDULE三类来源;计划ID、触发记录、冻结版本及合并范围;人工重试仅一跳父任务。
|
||||||
- [HTTP路由](../../internal/transport/httpserver/history.go)提供GET /v1/tasks/{task_id}/source,沿用tasks:read和队列范围,拒绝查询参数。
|
- [HTTP路由](../../internal/transport/httpserver/history.go)提供GET /v1/tasks/{task_id}/source,沿用tasks:read和队列范围,拒绝查询参数。
|
||||||
- [独立SDK](../../sdk/go/task_source.go)验证响应ID、类型组合、版本和时间,保留uint64精度;没有数据库依赖或自动重试。
|
- [独立SDK](../../sdk/go/task_source.go)验证响应ID、类型组合、版本和时间,保留uint64精度;没有数据库依赖或自动重试。
|
||||||
- [接口手册](../api/task-source.md)、[普通提交](../../sdk/go/examples/tasks/main.go)、[计划](../../sdk/go/examples/schedules/main.go)及[人工恢复](../../sdk/go/examples/recovery/main.go)同步提供调用与输出。
|
- [接口手册](../api/task-source.md)、[普通提交](../../examples/tasks/main.go)、[计划](../../examples/schedules/main.go)及[人工恢复](../../examples/recovery/main.go)同步提供调用与输出。
|
||||||
|
|
||||||
来源独立读取,不改变Task/TaskSummary及Worker协议,不在派发路径增加关联查询。复用现有(namespace_id,task_id)触发唯一索引及计划版本键,不新增迁移、索引或持久化字段;平台仍为35个实体、289个中文注释字段。新只读结构的字段也均有中文说明。
|
来源独立读取,不改变Task/TaskSummary及Worker协议,不在派发路径增加关联查询。复用现有(namespace_id,task_id)触发唯一索引及计划版本键,不新增迁移、索引或持久化字段;平台仍为35个实体、289个中文注释字段。新只读结构的字段也均有中文说明。
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
| 立即、相对延迟、指定时间提交 | 立即受理、数据库时间计算delay_ms、绝对available_at;[HTTP事务用例](../../internal/storage/mysql/task_integration_test.go)核对同键并发、相对时长、非法组合与批量;[SDK真实执行](../../sdk/go/task_delay_integration_test.go)核对数据库创建/开始时间不早于保存的执行时间 |
|
| 立即、相对延迟、指定时间提交 | 立即受理、数据库时间计算delay_ms、绝对available_at;[HTTP事务用例](../../internal/storage/mysql/task_integration_test.go)核对同键并发、相对时长、非法组合与批量;[SDK真实执行](../../sdk/go/task_delay_integration_test.go)核对数据库创建/开始时间不早于保存的执行时间 |
|
||||||
| 批量受理、逐项恢复 | 最多32项/512 KiB,单项事务和身份复用,区分明确失败/未知/未尝试;[批量报告](phase-3-batch-tasks.md)记录部分已提交后的真实HTTP回执丢失及原键补交 |
|
| 批量受理、逐项恢复 | 最多32项/512 KiB,单项事务和身份复用,区分明确失败/未知/未尝试;[批量报告](phase-3-batch-tasks.md)记录部分已提交后的真实HTTP回执丢失及原键补交 |
|
||||||
| 幂等及不可变参数/策略 | 任务、身份回执、事件与审计同事务;同语义原回执、变更冲突;[任务用例](../../internal/storage/mysql/task_integration_test.go)核对16并发只创建一次和精确数字;新增可选字段的[旧摘要兼容](../../internal/storage/mysql/task_delay_test.go) |
|
| 幂等及不可变参数/策略 | 任务、身份回执、事件与审计同事务;同语义原回执、变更冲突;[任务用例](../../internal/storage/mysql/task_integration_test.go)核对16并发只创建一次和精确数字;新增可选字段的[旧摘要兼容](../../internal/storage/mysql/task_delay_test.go) |
|
||||||
| 改参数形成关联新任务 | 新提交身份和相同business_key,旧输入保持;[查询示例](../../sdk/go/examples/task-query/main.go)分别读取text/json任务核对输入。该关联不等于业务去重或retry_of,原任务需调用方决定如何处理 |
|
| 改参数形成关联新任务 | 新提交身份和相同business_key,旧输入保持;[查询示例](../../examples/task-query/main.go)分别读取text/json任务核对输入。该关联不等于业务去重或retry_of,原任务需调用方决定如何处理 |
|
||||||
| 单次/总时限、最晚开始、安全重试 | 冻结策略、有限退避及未知效果;[决策](../../pulse/retry/decision_test.go)、[真实事务](../../internal/storage/mysql/worker_retry_integration_test.go)核对预算/回滚/迟到;[全停恢复](phase-3-delay-recovery.md)保留退避、次数和截止,过期不补发 |
|
| 单次/总时限、最晚开始、安全重试 | 冻结策略、有限退避及未知效果;[决策](../../pulse/retry/decision_test.go)、[真实事务](../../internal/storage/mysql/worker_retry_integration_test.go)核对预算/回滚/迟到;[全停恢复](phase-3-delay-recovery.md)保留退避、次数和截止,过期不补发 |
|
||||||
| 等待/运行取消 | 持久化意图、预占阻止开始、运行收集结果、未知效果仍核查;[取消事务](../../internal/storage/mysql/worker_cancel_integration_test.go)验证开始竞争/回滚,[取消报告](phase-3-cancellation.md)包含自动SDK通知、丢通知及不协作处理器 |
|
| 等待/运行取消 | 持久化意图、预占阻止开始、运行收集结果、未知效果仍核查;[取消事务](../../internal/storage/mysql/worker_cancel_integration_test.go)验证开始竞争/回滚,[取消报告](phase-3-cancellation.md)包含自动SDK通知、丢通知及不协作处理器 |
|
||||||
| 人工核查与重试 | 专门权限、版本、原因/证据、安全声明;核查不改旧尝试,人工重试创建唯一直接后继;[恢复用例](../../internal/storage/mysql/task_recovery_integration_test.go)验证竞争、审计失败回滚、原输入/结果保留及管理所有权;[接口/故障报告](phase-3-task-recovery.md) |
|
| 人工核查与重试 | 专门权限、版本、原因/证据、安全声明;核查不改旧尝试,人工重试创建唯一直接后继;[恢复用例](../../internal/storage/mysql/task_recovery_integration_test.go)验证竞争、审计失败回滚、原输入/结果保留及管理所有权;[接口/故障报告](phase-3-task-recovery.md) |
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
3. 已知失败步骤与并行信号等待 → RearmWorkflowWait第二代,原截止不变 → 旧代次信号被拒绝 → RetryWorkflowStep只保留一次剩余尝试 → 解除核查 → 新任务和新代次信号完成流程。
|
3. 已知失败步骤与并行信号等待 → RearmWorkflowWait第二代,原截止不变 → 旧代次信号被拒绝 → RetryWorkflowStep只保留一次剩余尝试 → 解除核查 → 新任务和新代次信号完成流程。
|
||||||
4. 查询UNKNOWN → 核查为SUCCEEDED/PENDING → 原结果消费 → ContinueWorkflowPoll → ContinueWorkflow → 只创建第二业务轮并最终DONE;查询总截止不变。
|
4. 查询UNKNOWN → 核查为SUCCEEDED/PENDING → 原结果消费 → ContinueWorkflowPoll → ContinueWorkflow → 只创建第二业务轮并最终DONE;查询总截止不变。
|
||||||
|
|
||||||
实现和证据:[公共路由](../../internal/transport/httpserver/workflows.go)、[严格JSON测试](../../internal/transport/httpserver/workflows_test.go)、[初始化范围测试](../../internal/storage/mysql/workflow_bootstrap_integration_test.go)、[当前详情及审计范围测试](../../internal/storage/mysql/workflow_details_integration_test.go)、[独立SDK场景](../../sdk/go/workflow_instances_integration_test.go)、[示例源码](../../sdk/go/examples/workflow/main.go)。完整复现入口是[隔离MySQL脚本](../../scripts/test-mysql.sh),跳过的环境依赖测试不算通过。
|
实现和证据:[公共路由](../../internal/transport/httpserver/workflows.go)、[严格JSON测试](../../internal/transport/httpserver/workflows_test.go)、[初始化范围测试](../../internal/storage/mysql/workflow_bootstrap_integration_test.go)、[当前详情及审计范围测试](../../internal/storage/mysql/workflow_details_integration_test.go)、[独立SDK场景](../../sdk/go/workflow_instances_integration_test.go)、[示例源码](../../examples/workflow/main.go)。完整复现入口是[隔离MySQL脚本](../../scripts/test-mysql.sh),跳过的环境依赖测试不算通过。
|
||||||
|
|
||||||
## 剩余交付
|
## 剩余交付
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- 000024只新增派生范围表、不可更新触发器及7个复合索引;回填遍历DSL步骤块,保留原版本/实例。最终表结构外键为0,无级联。当前36个模型、311个直接持久化字段有中文注释。
|
- 000024只新增派生范围表、不可更新触发器及7个复合索引;回填遍历DSL步骤块,保留原版本/实例。最终表结构外键为0,无级联。当前36个模型、311个直接持久化字段有中文注释。
|
||||||
- HTTP严格解析参数;SDK与服务端保持独立module。独立示例发布等待流程、验证精确分页、取消结清,不需Worker,也不模拟业务成功。
|
- HTTP严格解析参数;SDK与服务端保持独立module。独立示例发布等待流程、验证精确分页、取消结清,不需Worker,也不模拟业务成功。
|
||||||
|
|
||||||
接口:[查询手册](../api/workflow-queries.md)。示例:[运行说明](../examples/workflow-query.md)及[源码](../../sdk/go/examples/workflow-query/main.go)。
|
接口:[查询手册](../api/workflow-queries.md)。示例:[运行说明](../examples/workflow-query.md)及[源码](../../examples/workflow-query/main.go)。
|
||||||
|
|
||||||
## 环境与命令
|
## 环境与命令
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
专项验证固定版本发布后不变、仅有单队列tasks:read也能查当前任务来源、读取完整流程/定义仍拒绝、跨命名空间/队列拒绝;逐项注入错误所有者、门控、处理器版本、激活所属流程/步骤、任务反向引用、步骤类型、固定版本及策略错误;测试同时出现TASK/POLL关联、POLL父任务错误轮次及所有者丢失。真实恢复链验证业务第1轮→恢复第1轮→第2轮→再次恢复第2轮,激活与业务轮分别计数,原历史可读。
|
专项验证固定版本发布后不变、仅有单队列tasks:read也能查当前任务来源、读取完整流程/定义仍拒绝、跨命名空间/队列拒绝;逐项注入错误所有者、门控、处理器版本、激活所属流程/步骤、任务反向引用、步骤类型、固定版本及策略错误;测试同时出现TASK/POLL关联、POLL父任务错误轮次及所有者丢失。真实恢复链验证业务第1轮→恢复第1轮→第2轮→再次恢复第2轮,激活与业务轮分别计数,原历史可读。
|
||||||
|
|
||||||
源码:[来源存储](../../internal/storage/mysql/task_workflow_source.go)、[关联损坏及权限测试](../../internal/storage/mysql/task_workflow_source_integration_test.go)、[TASK恢复](../../internal/storage/mysql/workflow_recovery_integration_test.go)、[POLL恢复](../../internal/storage/mysql/workflow_poll_recovery_integration_test.go)、[SDK协议验证](../../sdk/go/task_source_test.go)、[真实SDK链路](../../sdk/go/workflow_instances_integration_test.go)、[运行例子](../../sdk/go/examples/workflow/main.go)。完整复现入口:`make integration`,见[隔离脚本](../../scripts/test-mysql.sh)。
|
源码:[来源存储](../../internal/storage/mysql/task_workflow_source.go)、[关联损坏及权限测试](../../internal/storage/mysql/task_workflow_source_integration_test.go)、[TASK恢复](../../internal/storage/mysql/workflow_recovery_integration_test.go)、[POLL恢复](../../internal/storage/mysql/workflow_poll_recovery_integration_test.go)、[SDK协议验证](../../sdk/go/task_source_test.go)、[真实SDK链路](../../sdk/go/workflow_instances_integration_test.go)、[运行例子](../../examples/workflow/main.go)。完整复现入口:`make integration`,见[隔离脚本](../../scripts/test-mysql.sh)。
|
||||||
|
|
||||||
## 继续交付
|
## 继续交付
|
||||||
|
|
||||||
|
|||||||
49
docs/reports/phase-6-archive-scheduler.md
Normal file
49
docs/reports/phase-6-archive-scheduler.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# P6:周期归档、持久作业与管理页面
|
||||||
|
|
||||||
|
日期:2026-09-23。变更基于 `6eb429e`,新增 Schema 31。范围为通用周期归档,不包含源数据删除。
|
||||||
|
|
||||||
|
## 交付内容
|
||||||
|
|
||||||
|
- 每个命名空间七类独立周期计划,保存策略版本、卷身份、周期、批大小、容量、触发时间和未结清作业占位。
|
||||||
|
- 作业状态与随机租约持久化;周期错过合并一次;失败占位,管理员可原作业重试或关闭。变更、人工操作、作业受理和终态审计同事务。
|
||||||
|
- 卷身份文件、全卷锁与作业锁;每轮一个文件批次;固定作业身份,验证文件链与数据库已确认前缀后继续。文件先发布后确认,提交不明时保留原状态恢复。
|
||||||
|
- 单作业和全卷逻辑文件容量上限,目录按 256 个条目分块扫描,最多 100000 项;不删除文件腾空间。
|
||||||
|
- 独立「归档作业」页面:周期表单、七类启停概况、本页待处理与根记录数、作业分页、校验摘要、失败解释、原作业恢复/关闭。沿用单管理员登录。
|
||||||
|
- [部署、接口与恢复操作示例](../api/archive-scheduler.md);文件格式与独立 CLI 兼容。
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
|
||||||
|
| 场景 | 实际验证 |
|
||||||
|
| --- | --- |
|
||||||
|
| 并发触发 | 真实 MySQL,8 个独立存储调用竞争同一到期计划,仅生成 1 个作业 |
|
||||||
|
| 并发领取 | 8 个竞争者仅一个有效租约;过期后换代领取,原代次确认返回状态冲突 |
|
||||||
|
| 文件/数据库确认间隙 | 首批 2 条审计已持久化,尚未确认数据库;控制测试将租约设为过期,新代次确认文件前缀,再完成末批,最终 2 批/3 条、同一 ID,离线摘要一致 |
|
||||||
|
| 文件损坏 | 损坏首批后停止,数据库保留原确认的批数与摘要;恢复原字节后以原 ID 重试,重复操作只留一条重试审计 |
|
||||||
|
| 策略变化 | 修改保留窗口后旧作业以 POLICY_CHANGED 停止;旧计划不再触发,关闭旧作业并确认新策略版本后才创建后续作业 |
|
||||||
|
| 暂停周期 | 已在执行的作业可正常结清,停止新周期触发 |
|
||||||
|
| 容量与分页 | 3 条各含 600000 字节正文的真实审计;1 MiB 预算停止,提高到 4 MiB 后原作业完成;独立全卷预算阻止新输出;26 个作业分 20+6 两页,无重复 |
|
||||||
|
| 文件与卷约束 | 单测覆盖数据库链前缀不符、原目录缺失、卷身份不符、初始化临时文件恢复、另一节点持锁时启动、忙卷不调度、目录分块计数与符号链接拒绝 |
|
||||||
|
| 身份与来源限制 | 未登录、只有 Bearer、跨命名空间和跨来源写操作拒绝;普通列表不返回租约代次 |
|
||||||
|
| 容器端到端 | 隔离 MySQL + Orbit 镜像,管理员登录、策略保存、周期保存/原键重放、后台生成归档、SUCCEEDED 和摘要、登出后 401;原 Worker/重试/计划及独立离线归档核验仍通过 |
|
||||||
|
| 浏览器实际操作 | 隔离测试账号从页面选择审计类别、保存周期,自动完成 2 批/3 条;制造 1 MiB 容量失败,页面填写原因提高到 4 MiB,原作业完成 3 批/5 条;失败计数从1回到0,旧周期结果保留 |
|
||||||
|
| 数据模型 | 当前基线与前向迁移无外键;历史升级保留原数据和既有索引;归档后源审计仍完整 |
|
||||||
|
|
||||||
|
本批覆盖的是数据库竞争、受控过期与文件确认间隙,不把修改测试租约截止等同于生产等待五分钟或真实跨机房切换。独立 CLI 的实际 SIGKILL 恢复证据见[文件归档报告](phase-6-retention-jobs.md)。
|
||||||
|
|
||||||
|
## 回归入口
|
||||||
|
|
||||||
|
- `make test vet build docs-check`:通过。
|
||||||
|
- `make console-e2e`:通过;首次运行发现 HTTP 路由范围冲突,修复后重跑通过,已加入路由注册/认证/跨来源回归。
|
||||||
|
- `go test -race -count=1 -run '^TestIntegrationArchive' -v ./internal/storage/mysql`(隔离数据库):3 个专项全部通过;最终目录分块扫描实现再次通过此回归。
|
||||||
|
- 最终文件执行器、配置和 HTTP 包的 `go test -race`:通过。
|
||||||
|
- `make integration`:通过。存储全量 305.578 秒,最终 SDK 进程故障组 132.167 秒、流程暂停全停组 12.906 秒;旧 Worker、订阅、调度、流程和两个独立业务模块回归通过。
|
||||||
|
|
||||||
|
新增容量夹具最初使用 UPDATE 填充旧审计,被已有 append-only 触发器拒绝;已修正为插入时写入大正文,未放宽审计不可改写约束。
|
||||||
|
|
||||||
|
## 当前边界
|
||||||
|
|
||||||
|
- 每次周期是新的候选快照,跨作业可能重复归档同一源记录。只有作业内分页固定截止和位置。
|
||||||
|
- 保留策略调整不会自动改变旧作业;关闭失败作业保留全部文件和历史。
|
||||||
|
- 首份清单写入前中断时,恢复不会重建缺失目录,需核查后关闭旧作业再生成新作业。
|
||||||
|
- 根目录物理空间、备份介质、对象存储与源库清理不在本批交付内;容量上限按逻辑文件字节计,不替代文件系统配额。
|
||||||
|
- 同卷接管要求所有节点访问同一支持所需 POSIX 语义的物理卷。隔离测试和本机 Docker 结果不代表任意网络文件系统或目标生产跨故障域验收。
|
||||||
43
docs/reports/phase-6-retention-jobs.md
Normal file
43
docs/reports/phase-6-retention-jobs.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 持久文件归档作业回归(2026-09-23)
|
||||||
|
|
||||||
|
本批在 `2d8467e` 的只读归档导出之上增加本地持久作业执行器及运维 CLI。无数据库迁移、外键或源数据清理操作;控制台导出的授权方式不变。使用说明与参数见[文件归档作业](../api/retention-jobs.md)。
|
||||||
|
|
||||||
|
环境:macOS arm64、Go 1.27.1、本地 POSIX 文件系统;Docker MySQL 8.4 与 Linux 容器。证据仅覆盖隔离环境中的文件恢复和数据库行为,不代表共享存储或跨节点归档高可用已经验收。
|
||||||
|
|
||||||
|
## 文件与进程恢复
|
||||||
|
|
||||||
|
`go test -race ./internal/retention ./cmd/orbit-admin` 覆盖:
|
||||||
|
|
||||||
|
| 场景 | 核对内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 有限批次与续跑 | 首次仅一批,保留原作业 ID;下次仅导出第二批;完成后重复调用不重复导出 |
|
||||||
|
| 崩溃残留 | 部分 `.write.tmp` 不算检查点,恢复会重新导出未发布批次 |
|
||||||
|
| 真实进程强杀 | 子进程第一批落盘后发出就绪标记,父进程 Kill 并 Wait 后,新进程取得锁并只继续第二批 |
|
||||||
|
| 文件链损坏 | 正文改动、缺批、互换、符号链接、未知文件全部阻止核验与续跑 |
|
||||||
|
| 容量上限 | 第一批已保存,第二批超过 1 MiB 总额时停止;显式提高预算后继续,第一批原字节不变 |
|
||||||
|
| 写入失败 | 在非 root 进程中把目录改为只读,第二批写入失败但原检查点仍可核验;恢复写权限后续跑 |
|
||||||
|
| 取消与并发 | 批次返回后上下文取消不发布文件;相同目录第二个写者被非阻塞文件锁拒绝 |
|
||||||
|
| 独立可信摘要 | 无 DSN 也能校验;与另行保存的末端摘要不匹配时非零退出 |
|
||||||
|
|
||||||
|
每个批次先同步正文,再原子发布、同步目录并回读。文件权限为 0600,目录为 0700。SHA-256 和前序摘要用于完整性校验;没有独立可信末端摘要时,不声称能抵抗整套档案与摘要一起被重写。
|
||||||
|
|
||||||
|
## Docker 首次部署闭环
|
||||||
|
|
||||||
|
`make console-e2e` 从空库迁移、管理员初始化和登录开始,保留原任务/重试/计划/退出后拒绝访问验证,增加:
|
||||||
|
|
||||||
|
- 使用刚保存的保留策略生成一个合法空作业,`completed=true`、`batches=1`、`roots=0`。
|
||||||
|
- 在 `--network none` 的独立容器运行 `retention verify-archive`,不注入 DSN;结果与运行输出逐字段一致。
|
||||||
|
|
||||||
|
数据库提交后的导出审计与文件持久化不是同一事务。文件失败可能导致重试产生新的导出审计;不会改写源记录,不能以审计存在推断文件已经保存。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
本批交付可重复执行的有限批次执行器,不新增周期调度服务或控制台作业列表。本地同目录文件锁不能证明跨节点共享文件系统可靠;不同节点独立磁盘也不能互相恢复。新一轮扫描需要独立目录,归档介质备份、定时触发、长期容量、跨节点接管及清理执行器仍需后续实现和验收。
|
||||||
|
|
||||||
|
## 真实 MySQL 作业结果
|
||||||
|
|
||||||
|
标准集成入口中的 `TestIntegrationRetentionDiskGrowthAndPreview` 使用 256 条样本,其中 254 条终态且完成时间相同,另 2 条为未知效果/运行租约保护对象。首次文件作业只导出 37 条,下一次从原目录接续,最终 254 条根记录、7 个批次、7 条 `archive:<job_id>` 审计;离线核验返回相同摘要和计数。停用命名空间返回 `PERMISSION_DENIED`,不能继续导出。控制台原有分页仍为 8 批、254 个唯一根记录,源表保持 256 条。
|
||||||
|
|
||||||
|
有界磁盘采样观察到表/索引统计 1,835,008 → 16,564,224 字节,增量 14,729,216;预览仍为 100 条样本且有后续候选。该统计只证明本次样本增长,不用于推算生产容量。
|
||||||
|
|
||||||
|
最终门禁:`make test`(四个模块 race)、`make vet`、`make build`、`make docs-check`、`make console-e2e` 和修正断言后的完整 `make integration` 均通过。全量 MySQL 存储组用时约 286 秒,真实 SDK Worker/Event 进程故障组约 130 秒,最后的双节点流程暂停恢复组约 13 秒;包含独立 Inbox/Outbox 业务示例、Schema 30 及零外键验证。首轮集成中停用命名空间的预期错误码写成了 `FORBIDDEN`,实际服务端正确返回 `PERMISSION_DENIED`;修正测试后重跑全链路通过,没有放宽权限判断。
|
||||||
49
docs/reports/phase-6-stability-p1.md
Normal file
49
docs/reports/phase-6-stability-p1.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# P1 稳定性与历史归档定向回归
|
||||||
|
|
||||||
|
这份报告定义当前代码的可复现入口,命令成功才算目标环境完成验证。它不把本机测试结果当作生产容量承诺。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ORBIT_TEST_MYSQL_DSN='root:...@tcp(127.0.0.1:3306)/orbit_test' \
|
||||||
|
ORBIT_TEST_SERVER_BINARY="$PWD/bin/orbit" \
|
||||||
|
ORBIT_TEST_WORKER_URL='http://127.0.0.1:8080' \
|
||||||
|
ORBIT_TEST_WORKER_CREDENTIALS_FILE="$PWD/.local/worker.credentials.json" \
|
||||||
|
make stability-check
|
||||||
|
```
|
||||||
|
|
||||||
|
入口覆盖:
|
||||||
|
|
||||||
|
- `TestIntegrationEventProcessCrossNodeReconnectStorm`:在两个独立 Orbit 进程之间反复切换 WebSocket 上游,验证重连风暴期间一次处理、一次确认和一次持久交付尝试。
|
||||||
|
- `TestIntegrationEventProcessSlowConsumerPeerProgress`:节点 A 的容量 1 处理器阻塞时,节点 B 仍可推进自己的交付,确认慢消费者隔离。
|
||||||
|
- `TestIntegrationWithTxRetriesPreCommitDeadlock`:两个真实 MySQL 事务反向加锁,使用数据库锁竞争和事务重试指标确认提交前死锁被有界恢复。
|
||||||
|
- `TestIntegrationFairDispatchApplicationIsolation` 与 `TestIntegrationWorkflowVersionAndRaces`:真实锁等待和多节点竞争路径,不使用固定 sleep 推断锁结果。
|
||||||
|
- `TestIntegrationRetentionDiskGrowthAndPreview`:写入带终态时间的有界任务样本,读取 `information_schema.TABLES` 的数据/索引字节增长,并核对保留策略预览只返回安全候选。
|
||||||
|
|
||||||
|
保留策略由数据库按命名空间保存,默认关闭归档。控制台单管理员通过“保留策略”页面修改窗口,要求版本、幂等键和原因;审计记录保留前后快照。导出是固定表目录的只读 JSON 快照,带 SHA-256 和策略版本,源数据仍保留。未确认交付、未知效果、未结束任务/流程、未处理 Inbox 和有效幂等记录不会成为普通归档候选。最终删除执行器、归档介质和目标拓扑 RPO/RTO 需要单独验收。
|
||||||
|
|
||||||
|
## 本地实测记录(2026-09-23)
|
||||||
|
|
||||||
|
环境为 macOS / arm64、Go 1.27.1、Docker MySQL 8.4.10。以下是隔离测试结果,不是百万企业容量或跨故障域高可用承诺。
|
||||||
|
|
||||||
|
上一批实现 `2f95d9c` 完成 `make test`、`make vet`、`make build`、`make docs-check` 与完整 `make integration`,包括独立 SDK 示例、真实进程故障及升级检查:
|
||||||
|
|
||||||
|
| 场景 | 观察结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 跨节点重连 | 两个独立进程间发生 4 次重连,业务处理 1 次,持久交付尝试 1 次 |
|
||||||
|
| 慢消费者 | 节点 A 消费阻塞期间,节点 B 仍完成独立交付 |
|
||||||
|
| 提交前死锁 | 真实反向加锁产生死锁,观察到 1 次有界重试后恢复 |
|
||||||
|
| 锁等待隔离 | 12 个阻塞连接参与应用公平派发验证,其他应用继续推进 |
|
||||||
|
| 新装与历史升级 | Schema 30;外键数量 0;v7 升级保留原 323 个索引组成项 |
|
||||||
|
| 有界磁盘增长 | 256 × 32 KiB 样本,表和索引统计从 1,818,624 增至 17,661,952 字节;候选预览 100 条、`more=true` |
|
||||||
|
|
||||||
|
本次继续补齐控制台归档分页和失败边界,在一次性数据库上运行 `go test -race -count=1 -run TestIntegrationRetention -v ./internal/storage/mysql`:
|
||||||
|
|
||||||
|
- `TestIntegrationRetentionPolicyIsSharedAuditedAndIdempotent`:不同 store 读取相同策略;相同幂等操作重放不增加修订号或重复写审计;旧版本修改被拒绝。
|
||||||
|
- `TestIntegrationRetentionDiskGrowthAndPreview`:256 条样本中 254 条终态,另 2 条保留为未知效果/运行中租约;终态记录采用相同完成时间验证复合游标。第一批 1 条,后续每批最多 37 条,共 8 批得到 254 条唯一根记录,源表仍为 256 条。
|
||||||
|
- 每批校验 SHA-256、策略版本及相同截止时间;跨类别/命名空间游标、无效游标、越界批量、业务 Bearer、旧策略和关闭策略均被拒绝。
|
||||||
|
- 此次物理统计增量为 12,238,848 字节(28,459,008 → 40,697,856)。该一次性数据库包含前面修正测试夹具时留下的样本,不能与上一轮绝对值直接比较;这里只验证有界写入增长及预览/导出的行为,不推导长期容量。
|
||||||
|
|
||||||
|
`make console-e2e` 在另一个空库从迁移与管理员初始化开始,验证:6 个已成功任务可见、WS Worker 排空、失败任务第 2 次成功、定时任务来源固定;保留策略保存/原键重放、旧版本 409、7 类预览、空快照摘要、退出后任务及策略接口 401 均通过。
|
||||||
|
|
||||||
|
本次 `make test`、`make vet`、`make build`、`make docs-check` 通过。服务端归档实现和迁移本次未修改,新增用例使用真实 MySQL 补充已有完整集成结果。
|
||||||
|
|
||||||
|
浏览器使用同一批隔离样本验证真实页面:关闭导出时仍可预览;存在未保存修改时禁用预览和导出;开启并保存后版本从 3 升至 4;首批 20 条、第二批累计 40 条且均通过浏览器 SHA-256 校验;切换类别重置进度并清除旧下载链接;空流程快照显示 0 条/已到末批且禁用继续导出。页面错误日志为空。开关点击区域和焦点提示已修正,截止时间带年份和 UTC,源数据不会被页面操作删除。
|
||||||
69
docs/reports/phase-7-acceptance-20260921.md
Normal file
69
docs/reports/phase-7-acceptance-20260921.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# P7 四项闭环验收记录
|
||||||
|
|
||||||
|
日期:2026-09-21。本文把流程恢复、运行观测、故障/容量和 SDK 接入放在同一次当前提交验收中。所有结果来自本地 Docker、一次性 MySQL 8.4 和 Go 1.27.1;它们证明代码链路可复现,不替代目标生产拓扑的容量、复制切换、RPO/RTO 和发布审批。
|
||||||
|
|
||||||
|
## 1. 真实流程恢复闭环
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make integration
|
||||||
|
```
|
||||||
|
|
||||||
|
关键结果:
|
||||||
|
|
||||||
|
- `TestIntegrationWorkflowRecovery` 通过 UNKNOWN 执行核查、步骤结论、继续、任务重试、POLL PENDING、SIGNAL/TIMER 等待恢复、旧证据保护、预算和截止时间保护、审计失败回滚及权限/修订冲突。
|
||||||
|
- `TestIntegrationWorkflowRuntimeProcess` 通过控制器全停、两个新进程接管、人工恢复后继续、POLL 同轮恢复和等待代次重建。
|
||||||
|
- `TestIntegrationWorkflowInstancesSDK` 通过公共 HTTP 与独立 SDK 的六类恢复命令、流程控制、信号、审计和真实 Worker 执行。
|
||||||
|
- WebSocket 结果交付、ACK 丢失、慢消费者、自然租约重投和跨节点接管均通过;完整 MySQL 集成退出 0。
|
||||||
|
|
||||||
|
控制台资源也使用同一公共接口:`/console/`、`/console/app.js`、`/console/styles.css` 返回 200,`NEEDS_REVIEW` 恢复入口要求当前修订、证据引用、原因、显式确认和幂等键。
|
||||||
|
|
||||||
|
## 2. 指标与告警
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test ./internal/observability ./cmd/orbit -count=1
|
||||||
|
docker run --rm --entrypoint /bin/promtool \
|
||||||
|
-v "$PWD:/work:ro" prom/prometheus:v3.5.0 \
|
||||||
|
check rules /work/deploy/monitoring/orbit.rules.yml
|
||||||
|
docker run --rm --entrypoint /bin/promtool \
|
||||||
|
-v "$PWD:/work:ro" prom/prometheus:v3.5.0 \
|
||||||
|
test rules /work/deploy/monitoring/orbit.rules.test.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:指标单元与真实进程采样通过;Prometheus 规则加载 8 条成功,触发、启动宽限、健康不报警、错误恢复和截断恢复测试通过。指标保持独立监听、固定基数、无业务 ID 标签;告警只触发调查,不自动改写任务或流程状态。
|
||||||
|
|
||||||
|
## 3. 故障、备份和容量样本
|
||||||
|
|
||||||
|
| 场景 | 命令 | 当前结果 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| MySQL 短暂不可用 | `scripts/test-db-recovery.sh` | readiness 在故障时 503,恢复后 200,恢复后任务可受理 |
|
||||||
|
| 逻辑备份恢复 | `scripts/test-backup-restore.sh` | 源库/恢复库核心计数一致,schema 28、外键 0、固定观测索引 6 |
|
||||||
|
| GTID 复制与手工提升 | `scripts/test-mysql-replication.sh` | 复制同步、源库停止、副本手工提升和原幂等键重放通过;自动切换仍未宣称完成 |
|
||||||
|
| 集中定时积压 | `ORBIT_BACKLOG_RUN_TIMEOUT=20s scripts/test-schedule-backlog.sh` | 256 个 occurrence 全部持久化,32 个计划均暂停确认,外键 0;保留有界曲线和指标采样,工具的预期 `tool_exit=1` 表示到达负向截止样本 |
|
||||||
|
|
||||||
|
长时积压样本继续使用 180 秒默认窗口;样本只用于比较积压清空、CPU/RSS、MySQL 资源和连接池曲线,不能直接外推生产吞吐或百万企业容量。
|
||||||
|
|
||||||
|
## 4. SDK 与业务接入
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make sdk-check
|
||||||
|
make protocol-check
|
||||||
|
make docs-check
|
||||||
|
make release-local-check
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:SDK 独立 module、`GOWORK=off`、竞态测试、vet、全包构建、协议版本检查和 149 个 Markdown 本地链接均通过;公共 SDK 的流程恢复、事件消费、Outbox/Inbox、提交回执丢失和 WebSocket 重连已有真实服务示例。新增[生产接入闭环](../sdk/production-integration.md),明确 Outbox、HTTP 受理、WebSocket 通知、Inbox 去重、错误分类和上线清单。
|
||||||
|
|
||||||
|
## 5. 仍需目标环境验收
|
||||||
|
|
||||||
|
- 生产硬件和混合负载下的 p50/p95/p99、磁盘、锁等待、复制延迟和容量余量。
|
||||||
|
- 自动故障切换、目标拓扑 RPO/RTO、备份介质和恢复工单。
|
||||||
|
- 生产监控通知、值班升级、凭据托管、日志脱敏和发布审批。
|
||||||
|
- 正式标签之间的完整 SDK/服务端双向兼容矩阵。
|
||||||
|
|
||||||
|
这些项目继续保留在[首版交付清单](../release-checklist.md)中,不能用本地回归结果替代。
|
||||||
53
docs/reports/phase-7-console-e2e.md
Normal file
53
docs/reports/phase-7-console-e2e.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# P7:Orbit Console 与真实业务链路回归
|
||||||
|
|
||||||
|
日期:2026-09-21。本批不是用页面假数据演示,而是在一次性 MySQL、真实 Orbit 进程和真实 Go SDK/Worker 上完成平台闭环,再核对控制台读取接口和静态资源。数据库继续禁止外键,测试结束后临时容器、凭据和构建目录均由标准脚本清理。
|
||||||
|
|
||||||
|
## 验收范围
|
||||||
|
|
||||||
|
标准命令:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/test-mysql.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
本次回归实际覆盖:
|
||||||
|
|
||||||
|
- 迁移、Schema 核验、无外键升级、健康检查和版本读取。
|
||||||
|
- HTTP 任务受理、幂等重报、查询、取消、延迟、批量和队列背压。
|
||||||
|
- Worker WSS 连接、领取、开始、续租、结果确认、重连、租约过期和进程强杀接管。
|
||||||
|
- 结果订阅创建、配置、事件过滤、WSS 交付、ACK、丢 ACK 恢复、租约过期、人工追加预算和重复确认。
|
||||||
|
- Weave 定义发布、固定版本启动、顺序/并行、定时等待、信号、流程事件审计、暂停/继续、取消、人工核查和进程全停恢复。
|
||||||
|
- 独立 Inbox、Outbox 业务示例,以及 SDK 在真实 HTTP/WSS 服务下的调用。
|
||||||
|
- Orbit Console `/console/`、`app.js`、`styles.css` 和五个 hash 视图的 HTTP 资源检查;详情页使用的流程、步骤、事件、订阅和交付 API 均由同一批真实数据回归覆盖。
|
||||||
|
|
||||||
|
## 结果
|
||||||
|
|
||||||
|
标准脚本退出码为 0。存储、协议、SDK、Worker 进程恢复、结果订阅进程恢复、流程全停恢复和两个独立业务模块均通过;日志中未出现失败测试。关键结果包括:
|
||||||
|
|
||||||
|
```text
|
||||||
|
schema_version=28 migration_count=28 foreign_key_count=0 complete=true
|
||||||
|
workflow state=SUCCEEDED result={"text":"HELLO ORBIT"}
|
||||||
|
workflow_timeline_verified=true signal_transition=PENDING->CONSUMED
|
||||||
|
worker_drained=true
|
||||||
|
management SDK exercised create/get/list/control/detail/expired retry
|
||||||
|
business commit verified before ACK; one business effect and two delivery attempts
|
||||||
|
paused task/signal/timer collected by fresh nodes; public SDK resume preserved version and deadlines
|
||||||
|
```
|
||||||
|
|
||||||
|
控制台资源和页面入口已在本机 Docker 服务确认:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/console/#/overview 200
|
||||||
|
/console/#/tasks 200
|
||||||
|
/console/#/schedules 200
|
||||||
|
/console/#/workflows 200
|
||||||
|
/console/#/subscriptions 200
|
||||||
|
/health/live {"status":"alive"}
|
||||||
|
/health/ready {"scope":"database_and_schema","status":"ready"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
这次回归证明真实服务链路、权限边界、事务恢复和控制台读取接口可以一起工作,不等于目标生产拓扑已经完成容量、自动故障切换、RPO/RTO、正式密钥托管、告警和值守验收。控制台仍使用调用方 Bearer Token,页面不直接连接 MySQL,也不把 WSS 租约秘密展示给管理页面。
|
||||||
|
|
||||||
|
下一批工作进入生产门槛:目标拓扑容量、慢消费者和重连风暴、MySQL 自动切换、备份恢复、滚动升级、正式凭据和跨版本兼容矩阵。
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 实现内容
|
## 实现内容
|
||||||
|
|
||||||
- 新增 [`sdk/go/examples/file-conversion`](../../sdk/go/examples/file-conversion/main.go):提交两行 CSV 快照,由版本化 `file.convert.csv-json` Worker 转成 JSON 结果。
|
- 新增 [`examples/file-conversion`](../../examples/file-conversion/main.go):提交两行 CSV 快照,由版本化 `file.convert.csv-json` Worker 转成 JSON 结果。
|
||||||
- 处理器只读取任务输入和执行上下文,使用稳定提交键;同一个运行身份重复提交不创建第二个任务。
|
- 处理器只读取任务输入和执行上下文,使用稳定提交键;同一个运行身份重复提交不创建第二个任务。
|
||||||
- 结果包含输出文件名、格式、转换正文和行数;输入标题重复、行列数不匹配或格式不支持时返回固定 `INVALID_FILE`,不把原始异常正文写入结果。
|
- 结果包含输出文件名、格式、转换正文和行数;输入标题重复、行列数不匹配或格式不支持时返回固定 `INVALID_FILE`,不把原始异常正文写入结果。
|
||||||
- 示例结束前等待 Worker 排空,任务与审计历史保留;单元测试覆盖正常 CSV、重复标题和列数错误。
|
- 示例结束前等待 Worker 排空,任务与审计历史保留;单元测试覆盖正常 CSV、重复标题和列数错误。
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ ORBIT_COMPAT_OLD_COMMIT=a50d5fa ./scripts/test-historical-sdk-compatibility.sh
|
|||||||
make mysql-compatibility
|
make mysql-compatibility
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本使用 Go 1.27.1 容器构建历史与当前的 `orbit`、`orbit-migrate`、`orbit-admin`,使用一次性 MySQL 8.4 数据库分别初始化两套 schema,再启动真实 HTTP 服务运行 `sdk/go/examples/tasks`。任务提交、重复幂等、查询、分页和取消都通过真实服务完成;数据库、容器、临时凭据和二进制在退出时清理。
|
脚本使用 Go 1.27.1 容器构建历史与当前的 `orbit`、`orbit-migrate`、`orbit-admin`,使用一次性 MySQL 8.4 数据库分别初始化两套 schema,再启动真实 HTTP 服务运行 `examples/tasks`。任务提交、重复幂等、查询、分页和取消都通过真实服务完成;数据库、容器、临时凭据和二进制在退出时清理。
|
||||||
|
|
||||||
## 实际结果
|
## 实际结果
|
||||||
|
|
||||||
|
|||||||
42
docs/reports/phase-7-sdk-remote-release.md
Normal file
42
docs/reports/phase-7-sdk-remote-release.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# P7:首个远端 SDK 接入版本
|
||||||
|
|
||||||
|
验证日期:2026-09-20。仓库为公开的 [k2safe/orbit](https://git.yuntuo.hk/k2safe/orbit),默认分支为 `main`。本批完成源码推送、独立 SDK 版本发布和真实外部项目取包验证。
|
||||||
|
|
||||||
|
## 发布身份
|
||||||
|
|
||||||
|
| 发布物 | Git 标签 | 不可变源码提交 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Orbit 服务端 | `v0.1.0` | `0e8b2ef2975df6b55f2240dd14eda83adf30ea94` |
|
||||||
|
| 独立 Go SDK | `sdk/go/v0.1.0` | `0e8b2ef2975df6b55f2240dd14eda83adf30ea94` |
|
||||||
|
|
||||||
|
通过公开 Git 远端读取标签并解引用,确认两个标签指向同一提交。仓库 API 返回 `private=false`、`empty=false`、`default_branch=main`。`main` 后续可以继续更新文档和代码,接入方应固定上述 SDK 版本。
|
||||||
|
|
||||||
|
## 外部项目验证
|
||||||
|
|
||||||
|
环境为 Go 1.27.1、darwin/arm64。在仓库外创建 `example.com/orbit-remote-consumer`,使用空 `GOMODCACHE`、独立 `GOPATH`、`GOWORK=off` 和 `GOENV=off`;未添加任何 `replace`。关闭 Go HTTP 身份认证及全局 Git 配置,SDK 通过公开代理下载。
|
||||||
|
|
||||||
|
本机 Go 进程直连代理发生网络超时。最终使用 `GOPROXY=https://goproxy.cn,direct`,并仅在验证进程中沿用系统已有的 HTTP/HTTPS 网络代理。保留 `GOSUMDB=sum.golang.org`,未设置 `GOPRIVATE` 或 `GONOSUMDB`,未关闭校验和验证,也未修改用户全局 Go/Git 设置。
|
||||||
|
|
||||||
|
| 验证 | 实际结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| `go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.0` | 从空缓存下载 SDK 和 `github.com/coder/websocket v1.8.15`,退出 0 |
|
||||||
|
| `go mod tidy` | 退出 0 |
|
||||||
|
| 外部项目 `go build` | 退出 0 |
|
||||||
|
| 外部项目运行 SDK HTTP smoke | 真实调用本地 `httptest` HTTP 服务的健康接口,输出 `remote_sdk_http_smoke=passed`,退出 0 |
|
||||||
|
| `go mod verify` | `all modules verified`,退出 0 |
|
||||||
|
| `go list -m -json` | `Version=v0.1.0`、`GoVersion=1.27.1`、无 `Replace` |
|
||||||
|
| 本地独立 SDK 门禁 | race、vet、build 通过;30 个包,禁止依赖为 0 |
|
||||||
|
| 协议与文档门禁 | 当前 HTTP/Worker/Event/schema 契约匹配,Markdown 本地链接检查通过 |
|
||||||
|
|
||||||
|
下载得到的模块校验和:
|
||||||
|
|
||||||
|
```text
|
||||||
|
git.yuntuo.hk/k2safe/orbit/sdk/go v0.1.0 h1:OJp+U8dDNKjiXy4ZhO4MMq68Ek8TkjphUAqsjOOg77c=
|
||||||
|
git.yuntuo.hk/k2safe/orbit/sdk/go v0.1.0/go.mod h1:9KlMW7Gy+EGaDoEwr3wHHFY47mnAymSfYeQpMS7h4ag=
|
||||||
|
```
|
||||||
|
|
||||||
|
业务接入命令、导入方式和场景见 [SDK 手册](../sdk/README.md#5-外部项目通过远端接入)。若网络无法访问默认模块代理,可单次执行 `GOPROXY=https://goproxy.cn,direct go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.0`;需要网络代理的环境应让 Go 进程使用可用的网络代理,不需要关闭模块校验。
|
||||||
|
|
||||||
|
## 结论与边界
|
||||||
|
|
||||||
|
业务项目可以固定 `v0.1.0` 进行开发和测试接入,无需取得本机源码或配置本地替换。本批验证远端发布与取包,没有重新声明生产环境验收完成;真实 MySQL/进程全量回归沿用[已记录结果](phase-7-regression-status.md),生产容量、数据库自动切换、完整跨标签矩阵和运维条件仍按[交付清单](../release-checklist.md)完成。
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user