Compare commits
33 Commits
sdk/go/v0.
...
ed17713ea1
| Author | SHA1 | Date | |
|---|---|---|---|
| ed17713ea1 | |||
| 4eeafdef41 | |||
| 385a618154 | |||
| 87aa710013 | |||
| 98ff028228 | |||
| 2ec13bf662 | |||
| 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 |
@@ -7,3 +7,8 @@ sdk
|
||||
examples
|
||||
deploy
|
||||
*.log
|
||||
.local
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
**/*.credentials.json
|
||||
|
||||
22
.env.example
22
.env.example
@@ -1,5 +1,7 @@
|
||||
# Example only. Load environment variables explicitly; binaries do not read .env.
|
||||
ORBIT_HTTP_ADDR=127.0.0.1:8080
|
||||
# 控制台默认队列,不替代登录及账号授权。
|
||||
ORBIT_CONSOLE_QUEUE_ID=
|
||||
# Set a real DSN through your secret manager. No credentials are committed.
|
||||
ORBIT_MYSQL_DSN=
|
||||
ORBIT_DB_MAX_OPEN=20
|
||||
@@ -18,6 +20,26 @@ ORBIT_EVENT_CONNECTION_LIMIT=128
|
||||
# 每个 WebSocket Hub 的数据库领取并发和有界内存排队,范围分别为1–256、1–65536。
|
||||
ORBIT_DISPATCH_WORKERS=4
|
||||
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.
|
||||
# ORBIT_METRICS_ADDR=127.0.0.1:9090. Never expose it on the public API ingress.
|
||||
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
|
||||
/.local/
|
||||
*.credentials.json
|
||||
|
||||
/examples/http-service/server
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- 任务来源通过独立只读路由查询,tasks:read仅授予当前任务的有限来源引用;计划定义仍需schedules:read。显式核对命名空间、队列、计划、触发与固定版本,悬空/错误/未实现所有者不得默认为DIRECT;不依赖外键,也不向派发路径加入来源查询。
|
||||
|
||||
- 业务事务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,人工重试不自动继承首次等待。
|
||||
|
||||
@@ -84,7 +85,7 @@
|
||||
- P6进程观测使用internal/observability固定指标目录,拒绝未知名称、类型和动态标签,不能引入任务/租户/业务键/凭据维度。ORBIT_METRICS_ADDR默认空(关闭),显式启用独立采集监听;业务端口不得暴露全局指标。渲染和网络I/O不持有全局记录锁,每个直方图局部快照保持桶/count/sum一致,最多2个并发抓取。启用指标后的5秒后台采样使用000026–000028固定索引读取任务、交付、流程状态、Weave Inbox状态、固定事件类别、终态耗时和最近窗口内固定事件阶段耗时,每个状态或事件最多10000条、事件窗口5分钟、900ms数据库截止;采样失败保留最后成功状态并独立记录错误/超时,不能影响业务事务或把有限样本当全局精确数。
|
||||
- HTTP成功升级在Hijack完成时记录握手,WS处理器存续计入独立连接指标;保持1xx临时响应、Flush、Hijack与原panic行为。DB池取本地Stats,抓取不查询业务库;后台批次success不是业务任务成功量。观测丢样或失败不得改变权威事务及恢复决定。
|
||||
- 进程指标、固定状态采样、格式验证、告警规则和标准回归证据见docs/reports/phase-6-observability.md及docs/reports/phase-6-business-observability.md。仍须补受理/授权/完成与流程推进延迟、持久吞吐/积压的全局聚合、重试/核查/恢复量、发送队列、实际采集告警及负载开销,不能把进程观测通过写成P6已完成。普通清理仍须保护有效幂等、未确认事件、未知结果和未终结流程。
|
||||
- 凭据轮换保留credential_id及订阅/Worker授权绑定,在同一事务替换摘要、固定到期时间、幂等回执和管理审计;新令牌必须重新建立会话,认证路径每次重新核对凭据状态。撤销必须让认证失效并保留原任务、订阅、交付、会话和审计证据,允许在应用或命名空间停用后执行;重复应用同一固定操作幂等返回。管理CLI先把固定操作文件写成0600,再提交;提交不明时保留文件并用原文件重放,不能重新生成秘密。日志、审计摘要、指标和业务表不得保存秘密原文或摘要。不要把轮换同一凭据ID解释成历史令牌仍有效,也不得级联删除订阅、任务或事件。
|
||||
- 凭据轮换保留credential_id及订阅/Worker授权绑定,在同一事务替换摘要、固定到期时间、幂等回执和管理审计;新令牌必须重新建立会话,认证路径每次重新核对凭据状态。撤销必须让认证失效并保留原任务、订阅、交付、会话和审计证据,允许在应用或命名空间停用后执行;重复应用同一固定操作幂等返回。管理CLI先把固定操作文件写成0600,再提交;提交不明时保留文件并用原文件重放,不能重新生成秘密。日志、审计摘要、指标和业务表不得保存秘密原文或摘要。000032 后仅在 previous_valid_until 之前接受上一代摘要(最长 30 天,不超过原到期时间);默认控制台/CLI过渡7天,旧固定v1操作仍立即轮换。默认新凭据长期有效;SET_EXPIRY 保持秘密不变,FINALIZE 停用旧代,REVOKE 同时停用两代;有效过渡期禁止再次轮换挤掉旧代。不要把稳定凭据ID解释成所有历史令牌仍有效,也不得级联删除订阅、任务或事件。
|
||||
- `sdk/go/cmd/orbit-load` 只用于隔离环境受理测量:每项使用稳定独立幂等键,固定请求形状和并发上限,按请求超时,不自动重试,不启动Worker或执行外部副作用;输出延迟百分位与有限错误分类,失败必须以非零状态返回。其结果不能替代真实Worker、流程推进、数据库拓扑或生产容量验收。
|
||||
- `sdk/go/cmd/orbit-exec-load` 独立测量真实Worker执行:每次运行固定专属处理器/业务键/序号/模拟时长,最多一次尝试且无外部副作用;任务成功须核对持久化结果、唯一尝试、时间顺序及单次本地调用。队列额度只读,提交不重试;整体截止和有界排空后输出实际分母,未完成/未知结果必须非零退出。数据库阶段耗时不得混用客户端轮询观察时间,不将隔离样本解释为生产容量。
|
||||
- `sdk/go/cmd/orbit-workflow-load` 独立测量真实Weave流程:每次运行创建固定版本的双分支并行定义,启动实例不自动重试,必须核对流程/步骤/任务/尝试/来源/输出及两倍分支调用;队列只读,处理器无外部副作用,截止后保留实际分母并以非零状态退出。隔离样本不代表生产容量、高可用或跨故障域验收。
|
||||
|
||||
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
|
||||
|
||||
.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:
|
||||
$(GO) fmt ./...
|
||||
cd sdk/go && $(GO) fmt ./...
|
||||
cd examples && GOWORK=off $(GO) fmt ./...
|
||||
cd examples/result-inbox && 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:
|
||||
$(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/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:
|
||||
$(GO) vet ./...
|
||||
cd sdk/go && $(GO) vet ./...
|
||||
cd examples && GOWORK=off $(GO) vet ./...
|
||||
cd examples/result-inbox && 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:
|
||||
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-migrate ./cmd/orbit-migrate
|
||||
$(GO) build -o bin/orbit-admin ./cmd/orbit-admin
|
||||
@@ -32,6 +44,18 @@ image:
|
||||
integration:
|
||||
./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:
|
||||
./scripts/test-mysql-replication.sh
|
||||
|
||||
@@ -56,3 +80,6 @@ release-local-check:
|
||||
# Runs the bounded, intentionally incomplete persistent backlog rehearsal in an isolated MySQL.
|
||||
schedule-backlog:
|
||||
./scripts/test-schedule-backlog.sh
|
||||
|
||||
adapters-check:
|
||||
./scripts/test-adapters.sh
|
||||
|
||||
309
README.md
309
README.md
@@ -1,112 +1,281 @@
|
||||
# Orbit · 星轨
|
||||
|
||||
Orbit 是面向多个业务项目的通用任务调度与流程编排平台,采用 **Go + MySQL + HTTP/WebSocket**。
|
||||
**Go + MySQL 构建的任务调度与流程编排平台。** 业务应用通过 HTTP 提交任务,Worker 通过 WebSocket 执行,结果通过可靠订阅交付;任务状态、计划、流程检查点与审计保存在 MySQL。
|
||||
|
||||
- **Pulse · 脉动**:异步任务、定时调度、执行授权与安全重试。
|
||||
- **Weave · 织流**:研发定义发布流程,管理步骤、等待、有界查询与恢复。
|
||||
- 业务处理器运行在接入方 Worker 中;平台不内置行业规则。
|
||||
- **Pulse · 脉动**:异步任务、延迟执行、定时计划、执行授权、重试与结果订阅。
|
||||
- **Weave · 织流**:研发定义并发布流程,支持顺序、并行、条件分支、等待信号、有界查询和人工恢复。
|
||||
- **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
|
||||
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、场景与验证报告
|
||||
```
|
||||
### Pulse:任务与调度
|
||||
|
||||
## 本地运行
|
||||
| 功能 | 做什么 / 怎么用 | 可运行示例 / 接口 |
|
||||
| --- | --- | --- |
|
||||
| 即时任务与提交幂等 | `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
|
||||
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_HTTP_ADDR='127.0.0.1:8080'
|
||||
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
|
||||
```
|
||||
|
||||
管理员可在「应用接入」页面新建应用和初始队列、查看和调整有效期、平滑换证(默认长期 Token,换证时新旧共存 7 天),并复制或下载 SDK 配置。设置 `ORBIT_CONSOLE_APPLICATION_ADMIN=admin` 指定唯一接入管理员;本地 Compose 已启用。[应用接入使用与接口文档](docs/api/console-applications.md)。
|
||||
|
||||
初始化只运行一次,后续重启复用数据库、账号和 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
|
||||
```
|
||||
|
||||
上方 DSN 为本地占位示例,需替换为实际凭据。部署时使用秘密管理和经过验证的 TLS 配置;迁移账户与运行账户分开授权。运行程序不自动迁移数据库。
|
||||
打开 **<http://127.0.0.1:8080/console/>**,用 `admin` 和刚设置的密码登录。页面自动使用当前访问地址请求服务端,统计与列表从平台接口读取 MySQL 中的数据。
|
||||
|
||||
另一个终端执行 SDK 示例:
|
||||
### 4. 跑一条完整执行链
|
||||
|
||||
另开终端,从仓库根目录执行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
ORBIT_URL=http://127.0.0.1:8080 go run ./examples/health
|
||||
export ORBIT_URL=http://127.0.0.1:8080
|
||||
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
|
||||
make fmt
|
||||
make test
|
||||
make vet
|
||||
make build
|
||||
make image
|
||||
make integration
|
||||
make protocol-check
|
||||
make docs-check
|
||||
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.1
|
||||
# 业务事务需要 MySQL Outbox 时再安装:
|
||||
go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@v0.1.1
|
||||
```
|
||||
|
||||
首版发布前按[交付核对清单](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)
|
||||
- [数据模型与约束](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)
|
||||
导入路径为 `orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"`。处理器的名称、处理器版本和载荷版本必须与 Worker 注册能力一致;没有匹配的在线 Worker 时,提交成功也不会立即执行。
|
||||
|
||||
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 与域名部署
|
||||
|
||||
本机给其他服务联调,可直接运行 `./scripts/orbit-local.py up`,自动部署到 `18080`,生成管理员和 SDK 配置;完整地址、Docker 网络与启停方法见[本地 Compose 测试环境](deploy/local/README.md)。
|
||||
|
||||
仓库提供 [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
|
||||
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)
|
||||
defer stop()
|
||||
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 {
|
||||
t.Fatalf("build sender: %v %s", err, raw)
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func TestIntegrationSenderCommitResponseLoss(t *testing.T) {
|
||||
for _, phase := range []string{"enqueue", "claim", "confirm"} {
|
||||
t.Run(phase, func(t *testing.T) {
|
||||
s := testStore(t, proxied)
|
||||
fresh, _ := New(direct, s.namespace, s.queue)
|
||||
fresh, _ := businessStore(direct, s.namespace, s.queue)
|
||||
id := testID(t)
|
||||
in := testRequest(s.queue)
|
||||
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 {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func TestIntegrationSenderHTTPReceiptLoss(t *testing.T) {
|
||||
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)
|
||||
r, err = wrong.SendOnce(ctx, through)
|
||||
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)
|
||||
_, 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 {
|
||||
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 {
|
||||
t.Helper()
|
||||
s, err := New(db, testID(t), testID(t))
|
||||
s, err := businessStore(db, testID(t), testID(t))
|
||||
if err != nil {
|
||||
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) {
|
||||
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) {
|
||||
t.Fatal("cross queue record returned", err)
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func TestIntegrationSenderBusinessRollbackAndConfirmationAtomicity(t *testing.T)
|
||||
}
|
||||
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'`)
|
||||
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)
|
||||
}
|
||||
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) }
|
||||
@@ -19,19 +19,23 @@ import (
|
||||
|
||||
func runCredential(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("usage: orbit-admin credential status|rotate|revoke|apply")
|
||||
return errors.New("usage: orbit-admin credential status|rotate|expiry|finalize|revoke|apply")
|
||||
}
|
||||
switch args[0] {
|
||||
case "status":
|
||||
return runCredentialStatus(args[1:])
|
||||
case "rotate":
|
||||
return runCredentialChange(args[1:], "ROTATE")
|
||||
case "expiry":
|
||||
return runCredentialChange(args[1:], "SET_EXPIRY")
|
||||
case "finalize":
|
||||
return runCredentialChange(args[1:], "FINALIZE")
|
||||
case "revoke":
|
||||
return runCredentialChange(args[1:], "REVOKE")
|
||||
case "apply":
|
||||
return runCredentialApply(args[1:])
|
||||
default:
|
||||
return errors.New("usage: orbit-admin credential status|rotate|revoke|apply")
|
||||
return errors.New("usage: orbit-admin credential status|rotate|expiry|finalize|revoke|apply")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +68,19 @@ func runCredentialStatus(args []string) error {
|
||||
|
||||
func runCredentialChange(args []string, action string) error {
|
||||
flags := flag.NewFlagSet("credential "+strings.ToLower(action), flag.ContinueOnError)
|
||||
namespace, credential, output, reason, validFor := "", "", "", "", "720h"
|
||||
namespace, credential, output, reason, validFor := "", "", "", "", "0"
|
||||
grace := time.Duration(0)
|
||||
if action == "ROTATE" {
|
||||
grace = 7 * 24 * time.Hour
|
||||
}
|
||||
var expected uint64
|
||||
flags.StringVar(&namespace, "namespace-id", "", "credential namespace id")
|
||||
flags.StringVar(&credential, "credential-id", "", "credential id")
|
||||
flags.Uint64Var(&expected, "expected-revision", 0, "current credential revision required for first apply")
|
||||
flags.StringVar(&reason, "reason", "", "required audit reason")
|
||||
flags.StringVar(&output, "output", "", "new private operation file; an existing file is never overwritten")
|
||||
flags.StringVar(&validFor, "valid-for", "720h", "new credential validity, from 1h to 8760h")
|
||||
flags.StringVar(&validFor, "valid-for", "0", "credential validity: 0 permanent, or 1h to 8760h")
|
||||
flags.DurationVar(&grace, "grace", grace, "rotation overlap: 0 immediate, up to 720h; default 168h")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -83,7 +92,7 @@ func runCredentialChange(args []string, action string) error {
|
||||
return errors.New("credential change requires valid ids and a nonzero --expected-revision")
|
||||
}
|
||||
duration := time.Duration(0)
|
||||
if action == "ROTATE" {
|
||||
if action == "ROTATE" || action == "SET_EXPIRY" {
|
||||
duration, err = time.ParseDuration(validFor)
|
||||
if err != nil {
|
||||
return errors.New("invalid --valid-for duration")
|
||||
@@ -95,7 +104,7 @@ func runCredentialChange(args []string, action string) error {
|
||||
}
|
||||
defer closeDB()
|
||||
op, err := mysql.PrepareCredentialOperation(ctx, db, mysql.CredentialChangeInput{
|
||||
NamespaceID: ns, CredentialID: id, ExpectedRevision: expected, Action: action, ValidFor: duration, Reason: reason,
|
||||
NamespaceID: ns, CredentialID: id, ExpectedRevision: expected, Action: action, ValidFor: duration, GraceFor: grace, Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("credential operation could not be prepared")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/buildinfo"
|
||||
@@ -26,13 +27,15 @@ func main() {
|
||||
}
|
||||
func run() error {
|
||||
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] {
|
||||
case "release":
|
||||
return runRelease(os.Args[2:])
|
||||
case "bootstrap":
|
||||
return runBootstrap(os.Args[2:])
|
||||
case "console-user":
|
||||
return runConsoleUser(os.Args[2:])
|
||||
case "verify":
|
||||
return runVerify(os.Args[2:])
|
||||
case "observe":
|
||||
@@ -42,7 +45,7 @@ func run() error {
|
||||
case "credential":
|
||||
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、凭据或业务数据。
|
||||
@@ -205,8 +208,16 @@ func runObserve(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" {
|
||||
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)
|
||||
var before string
|
||||
@@ -246,6 +257,7 @@ func runBootstrap(args []string) error {
|
||||
var in mysql.BootstrapInput
|
||||
var output string
|
||||
flags.StringVar(&in.Application, "application", "", "new application name")
|
||||
flags.DurationVar(&in.ValidFor, "valid-for", 0, "initial credential validity: 0 permanent, or 1h to 8760h")
|
||||
flags.Func("application-id", "existing application id; mutually exclusive with --application", func(v string) error {
|
||||
in.ExistingApplicationID = core.ID(v)
|
||||
return in.ExistingApplicationID.Validate()
|
||||
@@ -255,6 +267,11 @@ func runBootstrap(args []string) error {
|
||||
flags.StringVar(&in.Environment, "environment", "", "environment name")
|
||||
flags.StringVar(&in.Queue, "queue", "", "initial queue name")
|
||||
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.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")
|
||||
@@ -271,6 +288,20 @@ func runBootstrap(args []string) error {
|
||||
if flags.NArg() != 0 || output == "" {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -324,3 +355,78 @@ func runBootstrap(args []string) error {
|
||||
fmt.Println("Bootstrap committed. Credentials saved in the requested private file; valid for 30 days.")
|
||||
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/observability"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/retention"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/transport/httpserver"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
@@ -84,7 +85,9 @@ func run() error {
|
||||
txStats := mysql.ReadTransactionStats()
|
||||
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)))
|
||||
node, err := core.NewID()
|
||||
if err != nil {
|
||||
@@ -120,7 +123,39 @@ func run() error {
|
||||
httpserver.RegisterSchedules(server, schedule.New(schedules))
|
||||
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.RegisterConsoleRetention(server, consoleAuth, retentionStore, retentionStore)
|
||||
httpserver.RegisterConsoleArchiveJobs(server, consoleAuth, retentionStore, cfg.ArchiveStorageID)
|
||||
httpserver.RegisterConsoleApplications(server, consoleAuth, mysql.NewConsoleApplicationStore(db, cfg.ConsoleApplicationAdmin))
|
||||
httpserver.WrapConsoleAuth(server, consoleAuth)
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
103
deploy/docker-compose.yml
Normal file
103
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
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_APPLICATION_ADMIN: ${ORBIT_CONSOLE_APPLICATION_ADMIN:-}
|
||||
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:
|
||||
140
deploy/local/README.md
Normal file
140
deploy/local/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 本地 Docker Compose 联调环境
|
||||
|
||||
一套长期保留数据的 Orbit + MySQL 测试环境。项目名固定为 `orbit-test`,本机端口 `18080`;复用[基础 Compose](../docker-compose.yml),本目录补充镜像构建、跨服务网络和持久归档卷。
|
||||
|
||||
## 启动
|
||||
|
||||
此部署工具随 `main` 分支交付;使用当前 main 代码,SDK 仍可固定 v0.1.1。需要 Docker Desktop / Docker Compose、Python 3;首次构建镜像需要访问构建依赖。从 Orbit 仓库根目录执行:
|
||||
|
||||
```sh
|
||||
./scripts/orbit-local.py up
|
||||
```
|
||||
|
||||
脚本创建随机数据库密码和管理员密码,构建镜像,依次完成 MySQL 健康检查、前向迁移、运行账号权限收紧、结构核验、管理员/测试身份初始化及服务启动。成功后还检查 readiness 和实际 SDK 队列权限。
|
||||
|
||||
启动状态:
|
||||
|
||||
```sh
|
||||
./scripts/orbit-local.py status
|
||||
```
|
||||
|
||||
`mysql` 和 `orbit` 长期运行;`migrate`、`grant`、`verify`、`archive-init` 显示 `Exited (0)` 是一次性初始化成功。
|
||||
|
||||
## 控制台与接入地址
|
||||
|
||||
| 用途 | 地址/配置 |
|
||||
| --- | --- |
|
||||
| 本机控制台 | <http://127.0.0.1:18080/console/> |
|
||||
| 本机 API / SDK baseURL | `http://127.0.0.1:18080` |
|
||||
| Docker 网络 | `orbit-test-services` |
|
||||
| 同网络容器 API / SDK baseURL | `http://orbit-test:8080` |
|
||||
| Worker WebSocket | 上述主机的 `/v1/worker/connect`;SDK 自动处理升级与鉴权 |
|
||||
| 结果通知 | 使用 SDK 持久订阅 + EventConsumer,参见[消费手册](../../docs/sdk/event-consumer.md) |
|
||||
| 就绪检查 | `/health/ready` |
|
||||
| 管理员 | `admin` |
|
||||
| 管理员密码 | 本机 `.local/compose-test/admin.password` |
|
||||
|
||||
浏览器直接账号密码登录,自动使用同源地址,不需要上传凭据 JSON。MySQL 仅在环境内部网络开放,业务服务通过 Orbit API 接入。
|
||||
|
||||
## 为其他应用分配独立配置
|
||||
|
||||
登录后打开侧栏「应用接入」,新建应用并选择能力,成功后复制/下载该应用的 `.env`。详情可以随时查看队列、权限和有效期,重新生成 Token 会立即使旧 Token 失效,但保留原任务和订阅。完整 Token 仅在生成时显示。
|
||||
|
||||
`host.env` / `docker.env` 是初始 local-testing 应用的联调身份;独立应用应使用页面分配的自己的配置。应用接入管理只授权 `admin`,无需创建多角色。[详细操作和恢复手册](../../docs/api/console-applications.md)。
|
||||
|
||||
## 本机业务服务
|
||||
|
||||
初始化生成 `.local/compose-test/host.env`,包含:
|
||||
|
||||
```text
|
||||
ORBIT_URL=http://127.0.0.1:18080
|
||||
ORBIT_TOKEN=自动生成的真实令牌
|
||||
ORBIT_NAMESPACE_ID=自动生成的命名空间ID
|
||||
ORBIT_QUEUE_ID=自动生成的队列ID
|
||||
```
|
||||
|
||||
在仓库根目录加载,再启动自己的服务:
|
||||
|
||||
```sh
|
||||
set -a
|
||||
. ./.local/compose-test/host.env
|
||||
set +a
|
||||
# 在当前环境中启动你的业务进程
|
||||
```
|
||||
|
||||
这些变量由业务配置代码读取,再传给 SDK;SDK 本身不自动加载 env 文件:
|
||||
|
||||
```go
|
||||
client, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{
|
||||
Token: os.Getenv("ORBIT_TOKEN"),
|
||||
})
|
||||
if err != nil { return err }
|
||||
queueID := os.Getenv("ORBIT_QUEUE_ID")
|
||||
```
|
||||
|
||||
SDK 安装、任务处理器和完整服务见[根 README](../../README.md#sdk-怎么调用)、[HTTP 接入例子](../../examples/http-service/README.md)。业务处理器由接入服务注册运行;平台不会自动执行业务代码。
|
||||
|
||||
## 其他 Docker Compose 服务
|
||||
|
||||
在业务 Compose 中接入共享网络,并引用本机生成的 `docker.env`。将下方路径换为本机 Orbit 仓库的绝对路径;这份文件含真实令牌,不提交到业务 Git。
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
# 这里保留业务服务自己的 image/build、ports 等配置
|
||||
env_file:
|
||||
- /absolute/path/to/orbit/.local/compose-test/docker.env
|
||||
networks:
|
||||
- default
|
||||
- orbit-test
|
||||
|
||||
networks:
|
||||
orbit-test:
|
||||
external: true
|
||||
name: orbit-test-services
|
||||
```
|
||||
|
||||
业务服务读取到的 `ORBIT_URL` 为 `http://orbit-test:8080`,HTTP 和 WebSocket 都通过共享网络访问;Docker 内不要把 `127.0.0.1:18080` 当成宿主机地址。先启动 Orbit 环境,再启动业务 Compose。
|
||||
|
||||
如果仅临时联调一个已运行容器,可以执行 `docker network connect orbit-test-services <业务容器名>`,并在该业务服务配置中设置上述 URL 和凭据;容器重建后应采用 Compose 声明网络。
|
||||
|
||||
## 运行示例与看数据
|
||||
|
||||
宿主机有 Go 1.27.1 时,从仓库根目录执行:
|
||||
|
||||
```sh
|
||||
export ORBIT_URL=http://127.0.0.1:18080
|
||||
export ORBIT_CREDENTIALS_FILE="$PWD/.local/compose-test/sdk.credentials.json"
|
||||
cd examples
|
||||
go run ./worker
|
||||
go run ./schedules
|
||||
go run ./workflow
|
||||
go run ./event-protocol
|
||||
```
|
||||
|
||||
Worker 返回三条 `SUCCEEDED`;计划示例实际执行后暂停;流程示例完成并行、信号和审计核对;事件示例验证通知、ACK 与断线重报。刷新控制台即可查看真实数据。短示例每次产生新测试记录,不是长期业务消费者。
|
||||
|
||||
## 日常命令与持久化
|
||||
|
||||
```sh
|
||||
./scripts/orbit-local.py logs # 默认显示 Orbit 最近 100 行
|
||||
./scripts/orbit-local.py logs mysql # 数据库日志
|
||||
./scripts/orbit-local.py restart # 只重启 Orbit,等到 ready
|
||||
./scripts/orbit-local.py stop # 停止本环境的 Orbit 和 MySQL
|
||||
./scripts/orbit-local.py start # 启动已有容器,等到 ready
|
||||
./scripts/orbit-local.py up # 初次部署/重建镜像与容器;复用原密码和身份
|
||||
```
|
||||
|
||||
数据库卷为 `orbit-test_orbit-mysql`,归档卷为 `orbit-test_orbit-archives`,容器配置 `restart: unless-stopped`。停止、重启、重新构建都保留数据。私密配置目录 `.local/compose-test` 也要保留;仅留下卷而丢失密码文件不能当成全新安装。
|
||||
|
||||
归档卷预算 1 GiB,归档计划和保留策略通过控制台配置,默认不自动开启计划;目前导出保留源数据。不要为腾空间删除数据库卷。
|
||||
|
||||
默认使用 `10.253.240.0/24` 内部网络和 `10.253.241.0/24` 服务网络,避免占用已耗尽的 Docker 默认子网池。如与本机已有网络冲突,在私密 `compose.env` 中设置 `ORBIT_TEST_BACKEND_SUBNET`、`ORBIT_TEST_SERVICES_SUBNET` 后首次启动;已有网络需要先协调连接服务后再调整。
|
||||
|
||||
配置统一从 `.local/compose-test/compose.env` 读取,脚本隔离其他项目的同名环境变量。可查看原始配置:[基础文件](../docker-compose.yml) + [本地覆盖](compose.yml)。脚本不提供删卷命令;需手工管理时务必固定 Compose 项目 `orbit-test` 和同一份配置。
|
||||
|
||||
## 凭据和权限
|
||||
|
||||
新建 SDK 凭据默认长期有效(省略 `expires_at`);旧环境保留原策略,可在应用接入页「调整有效期」且不改变 Token。启动输出读取数据库当前有效期。凭据拥有此测试命名空间的任务、Worker、调度、订阅、流程及恢复权限;这是联调身份,正式环境按角色收窄。凭据到期/撤销后不会自动重新初始化或重置历史,按[轮换手册](../../docs/api/authentication.md)处理。
|
||||
|
||||
管理员密码、数据库密码、SDK Token 均保存在 `.local/compose-test` 下的私密文件;目录权限 0700、文件权限 0600,Git 和 Docker 构建上下文都排除这些文件。需要其他服务读取时分发 `host.env` / `docker.env`,不要给业务服务数据库管理员配置。
|
||||
59
deploy/local/compose.yml
Normal file
59
deploy/local/compose.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
# 叠加 deploy/docker-compose.yml;使用 scripts/orbit-local.py 固定项目和私密配置。
|
||||
services:
|
||||
archive-init:
|
||||
image: mysql:8.4
|
||||
restart: "no"
|
||||
user: "0:0"
|
||||
entrypoint: ["/bin/sh", "-ec"]
|
||||
command: ["chown 65532:65532 /archives && chmod 700 /archives"]
|
||||
volumes:
|
||||
- orbit-archives:/archives
|
||||
|
||||
bootstrap:
|
||||
profiles: ["tools"]
|
||||
image: ${ORBIT_IMAGE}
|
||||
user: ${ORBIT_LOCAL_USER}
|
||||
entrypoint: ["/usr/local/bin/orbit-admin"]
|
||||
command: ["verify"]
|
||||
environment:
|
||||
ORBIT_MYSQL_DSN: "root:${ORBIT_DB_ROOT_PASSWORD}@tcp(mysql:3306)/${ORBIT_DB_NAME}?parseTime=true&multiStatements=true"
|
||||
volumes:
|
||||
- ${ORBIT_LOCAL_DIRECTORY}:/run/local
|
||||
|
||||
orbit:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VERSION: ${ORBIT_BUILD_VERSION}
|
||||
REVISION: ${ORBIT_BUILD_REVISION}
|
||||
BUILD_TIME: ${ORBIT_BUILD_TIME}
|
||||
depends_on:
|
||||
archive-init:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
ORBIT_CONSOLE_APPLICATION_ADMIN: admin
|
||||
ORBIT_ARCHIVE_ROOT: /var/lib/orbit/archives
|
||||
ORBIT_ARCHIVE_STORAGE_ID: ${ORBIT_ARCHIVE_STORAGE_ID}
|
||||
ORBIT_ARCHIVE_MAX_BYTES: 1073741824
|
||||
ORBIT_ARCHIVE_TICK_INTERVAL: 10s
|
||||
volumes:
|
||||
- orbit-archives:/var/lib/orbit/archives
|
||||
networks:
|
||||
default:
|
||||
services:
|
||||
aliases: [orbit-test]
|
||||
|
||||
networks:
|
||||
default:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: ${ORBIT_TEST_BACKEND_SUBNET:-10.253.240.0/24}
|
||||
services:
|
||||
name: orbit-test-services
|
||||
ipam:
|
||||
config:
|
||||
- subnet: ${ORBIT_TEST_SERVICES_SUBNET:-10.253.241.0/24}
|
||||
|
||||
volumes:
|
||||
orbit-archives:
|
||||
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. 模块与依赖
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
## 3. 实体清单
|
||||
|
||||
当前共 36 个实体:初始迁移包含 30 个,000003 增加进程会话头和迟到证据,000010 增加应用共享额度,000013 增加结果流与不可变结果快照。000024 增加固定流程版本的查询范围。迁移自身另有 `orbit_schema_migrations` 记录。
|
||||
当前共 39 个业务实体:原运行模型为36个,000029增加控制台账号与会话,000030增加保留策略。迁移自身另有 `orbit_schema_migrations` 记录;控制台和策略表同样遵循零外键、中文 COMMENT、应用层归属校验。
|
||||
|
||||
| 模块 | Go 模型 | 数据表 |
|
||||
| --- | --- | --- |
|
||||
@@ -50,6 +50,9 @@
|
||||
| platform | `Subscription` | `orbit_subscriptions` |
|
||||
| platform | `Delivery` | `orbit_deliveries` |
|
||||
| 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 | `ExecutionGate` | `pulse_execution_gates` |
|
||||
| pulse | `WorkerSession` | `pulse_worker_sessions` |
|
||||
@@ -126,7 +129,7 @@ P2 服务端已新增会话隔离、两步派发、租约、并发许可、结
|
||||
|
||||
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)。
|
||||
|
||||
@@ -253,3 +256,21 @@ Event.WorkflowID为只读STORED生成列,从原不可变事件派生流程归
|
||||
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。
|
||||
|
||||
## 控制台身份与历史保留策略(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 持续开发与回归计划
|
||||
|
||||
更新:2026-09-20。此计划对应持续开发目标,由开发过程同步维护。数据库已按用户要求采用无外键设计,迁移 000008 移除历史外键,关联一致性由服务端事务保证。
|
||||
更新:2026-09-24。此计划对应持续开发目标,由开发过程同步维护。数据库已按用户要求采用无外键设计,迁移 000008 移除历史外键,关联一致性由服务端事务保证。
|
||||
|
||||
## 1. 最终交付目标
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
仓库内的开发回归统一入口为 `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. 持续推进方式
|
||||
|
||||
@@ -56,8 +78,8 @@
|
||||
| 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 |
|
||||
| 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);继续业务积压/阶段延迟、凭据轮换及其余生产门槛 |
|
||||
| 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` 已可在数据库核验前绑定候选二进制身份;仍需带标签的完整跨版本矩阵和目标拓扑验收 |
|
||||
| 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)、[四项闭环验收](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 是全量核对,不是最后才开始补文档。
|
||||
|
||||
@@ -85,10 +107,10 @@ P4/P5定时等待与后续查询轮次的实现及回归记录见[时钟报告](
|
||||
### 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)。
|
||||
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 定向验证旧令牌失效、新令牌重绑和零外键;仍需多节点安全重绑演练、凭据最小权限配置核对和生产密钥托管/审计演练。
|
||||
4. 已补[`orbit-admin retention inventory`](api/retention.md)只读盘点和[阶段报告](reports/phase-6-retention-inventory.md),能区分过期幂等、确定终态、未知效果、未终结流程、已确认/未确认交付、已消费/未消费Inbox、事件和审计规模;它不执行归档或删除。已补[容量模型](19-capacity-model.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 资源、连接池等待、业务快照和事务重试计数采样;下一步补长期存储增长、跨节点重连与目标数据库拓扑容量。
|
||||
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 资源、连接池等待、业务快照和事务重试计数采样;已补跨节点重连、慢消费者与有界磁盘增长样本(见 [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 验收。
|
||||
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 仍待验收。
|
||||
@@ -213,6 +235,8 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
|
||||
- [P2 SDK 协议阶段记录](reports/phase-2-sdk-protocol.md)
|
||||
- [P2 自动 Worker 运行器记录](reports/phase-2-worker-runtime.md)
|
||||
- [P2 执行历史与大载荷记录](reports/phase-2-execution-history.md)
|
||||
- [P1 稳定性与归档定向回归](reports/phase-6-stability-p1.md)
|
||||
- [持久文件归档作业回归](reports/phase-6-retention-jobs.md)
|
||||
|
||||
### 状态事件时间线子项
|
||||
|
||||
@@ -220,4 +244,48 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
|
||||
|
||||
### 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 均通过;main 与三组 v0.1.1 标签已推送,仓库外空缓存安装两个模块、构建、HTTP smoke 及校验和验证通过,无 replace;证据见[发布记录](reports/v0.1.1-release.md)。
|
||||
|
||||
### 本地 Compose 联调环境(2026-09-27)
|
||||
|
||||
- 已提供 `scripts/orbit-local.py` 和本地 Compose 覆盖文件,部署在独立项目 orbit-test,主机端口 18080;MySQL/归档卷持久保存,迁移账号与运行账号分离。
|
||||
- 初始化单管理员、完整测试身份和 host.env/docker.env,私密内容位于被 Git 与 Docker 构建排除的 .local/compose-test;重复启动复用原身份,不重置密码或数据。
|
||||
- 提供 orbit-test-services 共享网络与 http://orbit-test:8080 地址,固定可配置子网,解决本机默认 Docker 地址池耗尽的问题。
|
||||
- 已验证:真实容器构建/迁移/零外键/权限收紧;浏览器登录协议和未登录跳转;另一容器运行 Worker 完成三项任务及事件 ACK 重报;定时/流程示例;重复部署与 Orbit/MySQL 停止再启动后原任务、身份保持。
|
||||
- 接入、账号位置、启动/停止及网络声明见[本地部署手册](../deploy/local/README.md)。这是本地联调环境,不改变平台或 SDK 协议。
|
||||
|
||||
### 单管理员应用接入(2026-09-27)
|
||||
|
||||
- 新增独立应用接入页:应用/环境分页、队列及凭据详情、选择业务能力新建应用、重新生成 Token、复制/下载 SDK 配置;沿用单管理员,不引入角色系统。
|
||||
- 新增 Cookie 专用管理接口,部署显式指定唯一登录名;同事务校验管理员与有效会话,写入要求同源与专用请求头。Token 只在生成时交付,数据库只存摘要。
|
||||
- 新建资源/审计/幂等回执同事务,轮换复用现有凭据 ID 与轮换核心。请求不明保留原操作,原样重报;原有任务、订阅和交付身份不变。无新表、无外键、无 SDK 协议修改。
|
||||
- 已通过定向 MySQL 并发创建、幂等冲突、重名回滚、轮换/旧 Token 拒绝、退出失效与审计脱敏;真实 HTTP 已验证创建、轮换后原任务和订阅可读。标准/全量集成和页面验收记录见[本批报告](reports/console-applications.md)。
|
||||
- [页面使用、HTTP 接口及故障恢复](api/console-applications.md)。
|
||||
|
||||
### 长期凭据与平滑换证(2026-09-27)
|
||||
|
||||
- 新建应用和 bootstrap 默认长期 Token,可选定期;既有 Token 可通过管理端/CLI 原地调整有效期,无需改业务配置。
|
||||
- 000032 与基线增加旧摘要/固定截止,无外键。控制台/CLI 默认 7 天过渡;同一身份的 API、WS 控制命令和跨节点重连均核验两代有效性,权限与任务归属保持不变。
|
||||
- 结束过渡、两代同时撤销、过渡期防重复换证、旧 v1 固定操作兼容、幂等审计及中文模型注释随功能交付。
|
||||
- 验证结果见[长期凭据与平滑换证](reports/credential-overlap.md)。SDK 无接口修改;调用方须在截止前滚动更新配置并收尾在途执行。
|
||||
|
||||
@@ -21,10 +21,10 @@ docker image inspect registry.example/orbit:dev --format '{{.Id}}'
|
||||
|
||||
```sh
|
||||
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 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` 只是占位符,不是平台依赖。
|
||||
@@ -33,7 +33,7 @@ docker build --platform linux/amd64 \
|
||||
|
||||
```sh
|
||||
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` 对目标数据库逐项核对。两者不一致时停止发布,不通过人工修改清单来“对齐”。
|
||||
@@ -60,11 +60,12 @@ docker run --rm \
|
||||
--env ORBIT_MYSQL_DSN \
|
||||
--entrypoint /usr/local/bin/orbit-admin \
|
||||
-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 \
|
||||
--env ORBIT_MYSQL_DSN \
|
||||
--env ORBIT_HTTP_ADDR=0.0.0.0:8080 \
|
||||
--env ORBIT_CONSOLE_QUEUE_ID=queue_xxx \
|
||||
--env ORBIT_RUNTIME_BATCH_LIMIT=100 \
|
||||
--env ORBIT_WORKFLOW_TICK_INTERVAL=1s \
|
||||
--env ORBIT_SCHEDULE_TICK_INTERVAL=1s \
|
||||
@@ -79,6 +80,8 @@ docker run --rm \
|
||||
registry.example/orbit:dev
|
||||
```
|
||||
|
||||
`ORBIT_CONSOLE_QUEUE_ID` 是控制台的可选默认队列范围,只用于打开 `/console/` 时预填队列 ID,不是认证凭据。控制台登录使用数据库中的管理员账号,会话建立后服务端按账号绑定的业务凭据读取统计和任务;浏览器不需要填写 API 地址或上传凭据 JSON。多队列部署可以省略此变量,管理员在控制台内选择有权限的队列范围。首次管理员通过 `orbit-admin bootstrap` 创建,后续账号使用 `orbit-admin console-user create` 创建。
|
||||
|
||||
生产编排应先完成迁移和 `verify`,再启动新节点。服务收到 `SIGTERM` 后停止接收新连接并在有限窗口内关闭 HTTP、WebSocket 和后台循环;编排器的终止宽限期必须覆盖该窗口。启动后才能把节点加入流量池。
|
||||
|
||||
## 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) | 开发、测试回归、阶段验收与最终交付目标 |
|
||||
| [19 容量模型](19-capacity-model.md) | 企业规模到任务、流程、数据库与连接负载的推导及生产验收方法 |
|
||||
| [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) | 启动、发布、滚动停止、数据库恢复、备份和事故处理 |
|
||||
| [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) | 提交水位、冻结结果与可靠交付 |
|
||||
| [10 Weave决策与交付](10-workflow-engine-core.md) | 有限步骤决策和内部结果Inbox |
|
||||
| [11 Weave实例受理](11-workflow-admission.md) | 固定版本、初始图和首批任务事务 |
|
||||
@@ -30,6 +38,9 @@
|
||||
| [流程状态时间线](api/workflow-events.md) | 原始流程/步骤/信号状态事件、分页和历史详情 |
|
||||
| [接口手册](api/README.md) | 实际接口状态、调用说明与后续接口规划 |
|
||||
| [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) 为准;早期设计稿中的“未实现”描述保留其编写阶段语境。
|
||||
|
||||
[实例查询接口与SDK](api/workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
|
||||
|
||||
- [周期归档与作业管理](api/archive-scheduler.md)
|
||||
|
||||
- [周期归档与作业管理验证记录](reports/phase-6-archive-scheduler.md)
|
||||
|
||||
@@ -64,3 +64,7 @@ P1 业务接口已遵守下述约定,后续接口继续沿用:
|
||||
实现时必须补齐:用途与权限、HTTP 方法和路径、请求头、字段类型/必填/默认值/限制、完整请求与响应、错误码、幂等与并发规则、超时/重试/取消语义、SDK 对应方法、可运行示例、兼容版本和已覆盖的回归场景。
|
||||
|
||||
[流程实例筛选分页与权限](workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
|
||||
|
||||
- [周期归档与作业管理](archive-scheduler.md)
|
||||
|
||||
- [控制台应用接入与 Token 管理](console-applications.md)
|
||||
|
||||
@@ -121,7 +121,7 @@ curl -X POST "$ORBIT_URL/v1/application/resources:configure" \
|
||||
|
||||
## 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 \
|
||||
--application-id "$ORBIT_EXAMPLE_APP_ID" --namespace second --environment development \
|
||||
--queue second --resource-access --output .local/app-second.credentials.json
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/app-first.credentials.json \
|
||||
ORBIT_SECOND_CREDENTIALS_FILE=../../.local/app-second.credentials.json \
|
||||
go run ./examples/application-resources
|
||||
ORBIT_CREDENTIALS_FILE=../.local/app-first.credentials.json \
|
||||
ORBIT_SECOND_CREDENTIALS_FILE=../.local/app-second.credentials.json \
|
||||
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 个目录项,包含未完成、失败与已关闭作业;超过边界停止。并发同卷执行受全卷文件锁约束。它不等价于文件系统物理空间配额,不包含块分配/元数据开销,也不能约束绕过平台直接写目录的外部进程。生产使用独立持久卷,并监测实际磁盘余量。暂不提供文件删除、自动迁移介质、下载正文或对象存储接口。
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
状态:P1 已实现。业务接口每次在数据库事务内校验凭据摘要、撤销/到期、应用与命名空间启停,以及动作和队列范围。授权与写入使用同一事务;管理变更与已经获得授权锁的请求按事务先后次序生效。
|
||||
|
||||
已提供单管理员[应用接入页面](console-applications.md):新建应用、查看队列与凭据、长期凭据、调整有效期、平滑换证、复制 SDK 配置。下方 CLI 继续用于首次安装及运维。
|
||||
|
||||
## 初始化一个新的接入应用
|
||||
|
||||
前置:专用 MySQL 数据库已经执行 orbit-migrate up,使用持有初始化权限的数据库管理连接。此命令创建新应用、命名空间、队列和凭据,不会覆盖现有同名应用。
|
||||
@@ -19,7 +21,24 @@ go run ./cmd/orbit-admin bootstrap \
|
||||
|
||||
ORBIT_MYSQL_DSN 通过环境注入,命令不读取 .env。所有名称必填,以字母/数字开头,后续允许字母、数字、`_ . : -`;environment 最长 32,其余最长 128。输出文件必须不存在,以 0600 创建;原始凭据不会打印到 stdout/stderr。`.local/` 和 `*.credentials.json` 已被 Git 忽略。
|
||||
|
||||
输出 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。初始凭据默认长期有效(expires_at 省略),可用 `--valid-for 720h` 显式选择 30 天;0 表示长期,其他值为 1 小时至 365 天。权限包括当前命名空间内的五个 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)。
|
||||
|
||||
资源、凭据摘要和初始化审计在一个事务中提交。交付文件先写入并同步到磁盘,成功后再提交事务。如果文件写入失败则回滚并移除文件;如果已经交付后提交无法确认,保留文件并报错,应使用其中凭据核对结果,不能直接覆盖或重复创建。
|
||||
|
||||
@@ -27,7 +46,7 @@ ORBIT_MYSQL_DSN 通过环境注入,命令不读取 .env。所有名称必填
|
||||
|
||||
## 轮换与撤销
|
||||
|
||||
凭据轮换和撤销只允许在受保护的数据库管理环境执行,不提供公网管理接口。轮换保留 `credential_id`,因此订阅和 Worker 实例的授权绑定仍指向同一记录;新的 Worker 进程必须按现有进程身份规则重新连接,旧令牌不能再通过任何 API 或 WSS 认证。平台不会批量改写在途任务、交付记录或历史会话证据。
|
||||
管理 CLI 在受保护的数据库管理环境执行;控制台也可由部署指定的单管理员轮换凭据,见[应用接入](console-applications.md)。业务 SDK 不开放凭据管理,撤销仍由 CLI 执行。轮换保留 `credential_id`,因此订阅和 Worker 实例的授权绑定仍指向同一记录;新进程仍遵循原 Worker 身份规则;过渡期内旧 Token 的 API 请求、原 WS 控制命令及跨节点重连都继续有效。新 Token 立即可用,结束过渡或超过截止后旧 Token 才被拒绝。平台不会批量改写在途任务、交付记录或历史会话证据。
|
||||
|
||||
先读取脱敏状态和当前版本:
|
||||
|
||||
@@ -44,12 +63,13 @@ go run ./cmd/orbit-admin credential rotate \
|
||||
--namespace-id <namespace_id> \
|
||||
--credential-id <credential_id> \
|
||||
--expected-revision 1 \
|
||||
--valid-for 720h \
|
||||
--valid-for 0 \
|
||||
--grace 168h \
|
||||
--reason "scheduled credential rotation" \
|
||||
--output .local/rotated.credentials.json
|
||||
```
|
||||
|
||||
命令先读取并核对版本,生成一个固定 `operation_id`、令牌和绝对到期时间,写入新的 `0600` 私有操作文件,然后提交数据库事务。文件不得已存在;提交结果不明、网络断开或数据库返回未知错误时必须保留文件,并使用同一个文件执行 `credential apply --file`,不能重新执行 `rotate` 生成第二个秘密。`--valid-for` 限定在 1 小时到 1 年。数据库只保存秘密摘要、请求摘要和脱敏回执。
|
||||
命令先读取并核对版本,生成一个固定 `operation_id`、令牌、可选绝对到期时间和固定过渡截止,写入新的 `0600` 私有操作文件,然后提交数据库事务。文件不得已存在;提交结果不明、网络断开或数据库返回未知错误时必须保留文件,并使用同一个文件执行 `credential apply --file`,不能重新执行 `rotate` 生成第二个秘密。`--valid-for` 默认 0(长期),也可配置 1 小时到 365 天;`--grace` 默认 168h(7 天),范围 0–720h,0 表示立即停用旧 Token。旧 Token 的实际截止取过渡截止与原到期时间的较早者,不复活已过期秘密。数据库只保存秘密摘要、请求摘要和脱敏回执。
|
||||
|
||||
```sh
|
||||
go run ./cmd/orbit-admin credential apply \
|
||||
@@ -58,6 +78,36 @@ go run ./cmd/orbit-admin credential apply \
|
||||
|
||||
`apply` 会严格拒绝软链接、非 0600 文件、未知 JSON 字段、重复 JSON 对象和修改过的操作内容。重复应用已提交的文件只返回原回执,回执中的 `deduplicated` 为 `true`,不会重新轮换或新增审计。
|
||||
|
||||
### 原 Token 调整有效期
|
||||
|
||||
无需换 Token:读取当前 revision 后执行以下命令,业务侧继续使用原配置。缩短有效期同样会生效,应按预期截止配置。
|
||||
|
||||
```sh
|
||||
go run ./cmd/orbit-admin credential expiry \
|
||||
--namespace-id <namespace_id> --credential-id <credential_id> \
|
||||
--expected-revision 1 --valid-for 0 \
|
||||
--reason "service credential remains valid until revoked" \
|
||||
--output .local/expiry.operation.json
|
||||
```
|
||||
|
||||
### 平滑切换的完整顺序
|
||||
|
||||
1. 生成新 Token,保留默认 7 天过渡。凭据 ID、权限、任务、订阅和交付身份不变。
|
||||
2. 把新 Token 发布到调用方配置,逐台更新 API 服务、Worker 和结果消费者。SDK v0.1.1 的 `Options.Token` 在创建客户端时读取,修改 `.env` 不会自动刷新正在运行的客户端;新建客户端或滚动重启服务,按 [HTTP 宿主示例](../examples/http-service.md) 正常排空在途工作。
|
||||
3. 在截止前确认所有旧进程已完成任务回报和 ACK,并已经退出或用新 Token 重新连接。HTTP 调用无须整体停机;持续执行能力依赖业务侧保留足够副本。单实例重启本身会产生短暂停顿。
|
||||
4. 全部切换后可提前结束过渡;否则旧 Token 到时自动失效,认证使用数据库时间,不依赖定时清理。
|
||||
|
||||
```sh
|
||||
go run ./cmd/orbit-admin credential finalize \
|
||||
--namespace-id <namespace_id> --credential-id <credential_id> \
|
||||
--expected-revision 2 --reason "all service instances switched and drained" \
|
||||
--output .local/finalize.operation.json
|
||||
```
|
||||
|
||||
结束过渡立即停用旧 Token,当前 Token 不变。长期 Token 仍受主动撤销、应用/命名空间停用约束。旧代仍在有效过渡期时再次轮换返回 409,避免误伤未切换服务;先完成本次切换。修改当前有效期不延长旧代截止。若旧 Token 本身即将到期而需要更长切换窗口,应先调整当前有效期,再发起换证。
|
||||
|
||||
新固定操作文件使用 version=2;历史 version=1 仍按原来的固定到期和立即轮换语义重放,不自动延长旧秘密。
|
||||
|
||||
发生泄露或 Worker 必须立即停用时执行撤销:
|
||||
|
||||
```sh
|
||||
@@ -69,7 +119,7 @@ go run ./cmd/orbit-admin credential revoke \
|
||||
--reason "incident response"
|
||||
```
|
||||
|
||||
撤销同样先写入固定操作文件,再提交事务;提交不明时用 `credential apply --file .local/revoke.operation.json` 重试。撤销只标记凭据失效,不删除订阅、任务、交付或历史记录。即使应用或命名空间已停用,管理撤销仍可执行。重复应用同一撤销文件只返回原回执,不追加重复审计。审计主体取自 MySQL 管理连接的 `CURRENT_USER()`,审计摘要不包含秘密原文或秘密摘要。
|
||||
撤销同样先写入固定操作文件,再提交事务;提交不明时用 `credential apply --file .local/revoke.operation.json` 重试。撤销使当前和过渡中的上一代同时失效,不删除订阅、任务、交付或历史记录。即使应用或命名空间已停用,管理撤销仍可执行。重复应用同一撤销文件只返回原回执,不追加重复审计。审计主体取自 MySQL 管理连接的 `CURRENT_USER()`,审计摘要不包含秘密原文或秘密摘要。
|
||||
|
||||
## 凭据格式与作用域
|
||||
|
||||
@@ -111,3 +161,7 @@ P3 结果订阅核心新增 `events:read`、`events:manage` 和 `events:consume`
|
||||
P4定义发布新增`workflows:definitions:read`与`workflows:definitions:publish`,仅显式`--workflow-access`授予。创建/读取元数据作用于命名空间,发布及读取完整版本额外校验全部引用队列;默认提交/Worker凭据不获此权限。发布不创建任务,不要求tasks:submit,详见[定义API](workflow-definitions.md)。
|
||||
|
||||
P4/P5实例公共接入增加`workflows:start/read/control/signal/recover`及`workflows:audit:read`。五个独立CLI开关`--workflow-run-access`、`--workflow-control-access`、`--workflow-signal-access`、`--workflow-recovery-access`、`--workflow-audit-access`分别明确授予。前四个同时包含实例读取;audit只授予流程操作审计读取。原`--workflow-access`仍仅授予定义读/发布,任务`--recovery-access`不授予流程恢复。全部固定队列仍按当前凭据校验,详见[实例权限表](workflows.md#1-路由权限与-sdk)。初始化权限集合在交付凭据前校验。
|
||||
|
||||
## 部署升级
|
||||
|
||||
迁移 `000032` 为凭据表增加上一代摘要与 UTC 截止,并以 CHECK 约束成对为空;不新增外键、不改变已有秘密或有效期。新安装的基线同步到 32。按部署流程应用迁移并更新全部 Orbit 节点后再使用平滑换证;旧版本节点只认识当前摘要,不能在混合版本期间承诺旧 Token 可用。既有凭据不会被迁移自动改为长期,须通过有审计的 expiry 操作显式调整。
|
||||
|
||||
@@ -157,4 +157,4 @@ for _, item := range result.Items {
|
||||
|
||||
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. 可运行场景
|
||||
|
||||
源码:[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
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=/private/path/worker.credentials.json \
|
||||
go run ./examples/cancellation
|
||||
go run ./cancellation
|
||||
```
|
||||
|
||||
预期输出:
|
||||
|
||||
147
docs/api/console-applications.md
Normal file
147
docs/api/console-applications.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 控制台应用接入
|
||||
|
||||
单管理员在「应用接入」页面创建应用、初始队列与 SDK 凭据,查看环境、队列、权限和有效期,平滑换证,复制或下载 `.env` 配置。数据库只保存 Token 摘要:完整 Token 仅在新建或轮换时交付,列表和详情不能反查旧秘密。
|
||||
|
||||
## 1. 启用和使用
|
||||
|
||||
部署配置指定唯一有跨应用接入管理权限的控制台登录名:
|
||||
|
||||
```sh
|
||||
export ORBIT_CONSOLE_APPLICATION_ADMIN=admin
|
||||
```
|
||||
|
||||
该账号必须已经通过 bootstrap 或 console-user create 创建。配置为空时所有应用接入管理请求返回 403,不会自动提升已有账号;多节点使用相同配置。继续使用单管理员,不引入角色管理系统。现有控制台账号与会话的绑定凭据仍须有效;到期前可在本页轮换。轮换 SDK Token 不改变浏览器独立会话秘密。
|
||||
|
||||
本地 Compose 已为 `admin` 配置该入口,访问 `http://127.0.0.1:18080/console/#/applications`。
|
||||
|
||||
1. 点击「新建应用」,填写应用名称、环境、命名空间、初始队列和操作原因。
|
||||
2. 选择 Worker、结果订阅、定时、流程等需要的能力及有效期。基础权限包含任务提交/查询/取消、队列读取/管理。
|
||||
3. 创建成功后复制或下载完整 `.env`,交给该应用的服务端配置。默认地址使用当前页面来源,详情中可改为容器地址或部署域名。
|
||||
4. 已接入应用可以随时查看元数据和配置模板。模板中的 Token 是占位值,必须替换成已经保存的原 Token。
|
||||
5. 「平滑换证」保留 application_id、namespace_id、credential_id、队列、任务和订阅;新 Token 立即生效,旧 Token 在配置的过渡期内继续使用(页面默认 7 天,支持 1/7/30 天),便于逐台更新配置。详情展示旧 Token 实际截止,全部服务切换后可点「结束过渡」。
|
||||
6. 默认「长期有效」,不需要每月人工换证;已有凭据可点「调整有效期」直接设为长期,原 Token 保持不变。
|
||||
|
||||
环境与命名空间形成隔离边界。不同应用应分别创建;同一应用的多个命名空间在列表中分别展示。页面目前支持新建应用及其初始队列,未提供应用删除、身份迁移、跨应用任务列表或现有应用新增命名空间的表单;已有 CLI 的 `--application-id` 仍可增加命名空间,队列 SDK 可增加同范围队列。接入管理页不会改变其他业务页面的账号默认范围。
|
||||
|
||||
## 2. 路由与认证
|
||||
|
||||
这些是浏览器管理端接口,不属于业务 SDK。只接受有效的 `orbit_console_session` HttpOnly Cookie,并在数据库事务中重新核验部署指定的管理员账号和会话;即使 SDK 凭据拥有全部业务权限也不能访问。
|
||||
|
||||
| 方法与路径 | 用途 |
|
||||
| --- | --- |
|
||||
| `GET /console/applications?after=<namespace_id>` | 按命名空间 ID 升序读取 50 条接入范围,返回 `items`、可选 `next`;`after` 为排他起点 |
|
||||
| `GET /console/applications/{namespace_id}` | 应用/环境详情、最多 100 个队列和 100 张脱敏凭据;超出返回 `truncated=true` |
|
||||
| `POST /console/applications/prepare` | 校验意图,生成固定操作 ID、资源 ID、Token、绝对到期时间;不创建资源、不轮换秘密 |
|
||||
| `POST /console/applications/apply` | 提交准备结果,原样重报返回原脱敏回执 |
|
||||
|
||||
POST 必须有 `Content-Type: application/json` 和 `X-Orbit-Console: 1`;浏览器 Origin 必须匹配当前主机,拒绝跨站 Fetch Metadata,不提供跨域授权。响应禁止缓存,成功信封为 `{"data": ...}`。未知字段、重复 JSON 键、显式 null 和错误类型被拒绝,正文上限 512 KiB。
|
||||
|
||||
## 3. 新建应用
|
||||
|
||||
prepare 请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "CREATE",
|
||||
"application": "order-service",
|
||||
"namespace": "default",
|
||||
"environment": "test",
|
||||
"queue": "orders",
|
||||
"capabilities": ["worker", "events", "schedules", "workflows"],
|
||||
"expected_revision": "0",
|
||||
"valid_days": 0,
|
||||
"reason": "接入订单服务"
|
||||
}
|
||||
```
|
||||
|
||||
名称以字母或数字开头,后续只允许字母、数字、`_ . : -`,environment 最长 32,其余最长 128。valid_days 为 0–365 的整数,默认 0 表示长期有效;reason 非空、最多 1024 个 Unicode 字符,不含换行或 NUL。capabilities 可选 `worker`、`events`、`schedules`、`workflows`、`recovery`、`resources`,不可重复。新建不传 namespace_id / credential_id,expected_revision 省略或为字符串 `"0"`。
|
||||
|
||||
prepare 响应 data 为固定操作对象:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"input": {"kind":"CREATE","application":"order-service","namespace":"default","environment":"test","queue":"orders","capabilities":["worker","events"],"expected_revision":"0","valid_days":0,"reason":"接入订单服务"},
|
||||
"operation_id": "11111111111111111111111111111111",
|
||||
"application_id": "22222222222222222222222222222222",
|
||||
"namespace_id": "33333333333333333333333333333333",
|
||||
"queue_id": "44444444444444444444444444444444",
|
||||
"credential_id": "55555555555555555555555555555555",
|
||||
"token": "服务端生成的完整 Token"
|
||||
}
|
||||
```
|
||||
|
||||
以上是结构示意,ID、令牌及时间以服务端返回为准。将 **data 整体原样** POST 到 apply,不包 `data` 外层。应用、额度、命名空间、队列、凭据摘要、审计和幂等回执在同一事务提交。初始队列并发 4、开始速率 10/秒,沿用 bootstrap 默认策略;凭据队列范围为新命名空间的全部队列。重名应用返回 409,不覆盖原资源。
|
||||
|
||||
## 4. 平滑换证
|
||||
|
||||
prepare 请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "ROTATE",
|
||||
"namespace_id": "33333333333333333333333333333333",
|
||||
"credential_id": "55555555555555555555555555555555",
|
||||
"expected_revision": "1",
|
||||
"valid_days": 0,
|
||||
"grace_days": 7,
|
||||
"reason": "更新接入凭据"
|
||||
}
|
||||
```
|
||||
|
||||
expected_revision 为读取到的字符串版本。只允许上述轮换字段,不混入新应用名称或能力组。准备对象包含原 application_id / namespace_id / credential_id,没有 queue_id。apply 复用原有凭据轮换事务:将原摘要保留到固定过渡截止,替换当前摘要与可选到期时间,版本加一,保存脱敏回执及人类账号审计;权限组不改变。已撤销的凭据禁止变更。ROTATE 的 grace_days 范围 0–30,API 省略/0 保持立即停用旧 Token 的兼容语义,页面显式提交 7;实际截止不超过旧 Token 原到期时间。过渡未结束时再次轮换返回 409。
|
||||
|
||||
apply 成功 data:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "11111111111111111111111111111111",
|
||||
"application_id": "22222222222222222222222222222222",
|
||||
"namespace_id": "33333333333333333333333333333333",
|
||||
"credential_id": "55555555555555555555555555555555",
|
||||
"revision": "2",
|
||||
"deduplicated": false
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 调整有效期与结束过渡
|
||||
|
||||
prepare 输入示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "SET_EXPIRY",
|
||||
"namespace_id": "33333333333333333333333333333333",
|
||||
"credential_id": "55555555555555555555555555555555",
|
||||
"expected_revision": "2",
|
||||
"valid_days": 0,
|
||||
"reason": "服务凭据设置为长期有效"
|
||||
}
|
||||
```
|
||||
|
||||
SET_EXPIRY 不生成新 Token,只更新当前有效期;valid_days=0 为长期,1–365 为从准备时间计算的天数。FINALIZE 使用相同身份/版本字段,kind 改为 FINALIZE,valid_days 必须为 0,用于提前停用旧 Token。两者均禁止 grace_days 和新建字段,prepare 不返回 token;FINALIZE 也不返回 expires_at。apply 使用完整原对象,具有相同的并发版本、幂等和审计约束。
|
||||
|
||||
新版 prepare 对象带 version=2;expires_at 为空时**省略字段**,不要传显式 null。ROTATE 有过渡期时另外包含固定 previous_valid_until;详情中的 previous_valid_until 是受旧到期时间约束后的实际截止,未设置时省略。详情/回执不会返回旧秘密。历史不带 version 的固定操作仍按原语义重放。
|
||||
|
||||
完整滚动更新及 SDK 客户端生命周期说明见[凭据手册](authentication.md)。更换配置不会自动刷新已创建的客户端,须在过渡截止前完成重建或逐台重启并排空在途任务。
|
||||
|
||||
## 6. 超时、重复和审计
|
||||
|
||||
- 页面提交前把整个固定操作保留在当前内存;请求超时或结果未知时,按钮变成「重试确认」,重用同一份内容。不能再次 prepare 来代替核对。
|
||||
- 页面允许「保存待核对操作」下载私密 JSON;含真实 Token,应放到受限目录。恢复时登录同一管理员后,向 apply 发送该文件正文。不要把 Cookie 或操作文件提交 Git,也不要放到 URL。
|
||||
- 关闭/刷新页面会丢失内存中的 Token,页面会提醒先保存。若新建已经成功、交付内容未保存,可从应用列表重新生成同一凭据的 Token;不要再次创建同名应用。
|
||||
- 原操作返回的回执是历史快照,可能已被后续轮换替代。页面会重新读取当前凭据版本后才展示配置。
|
||||
- 管理审计主体是 `console:<user_id>`;正文只有权限、资源 ID、到期时间等元数据。列表、审计、幂等回执和业务记录都不保存秘密原文。
|
||||
- 当前管理员退出、会话过期或账号停用后不能重放管理操作。SDK Token 撤销/到期与控制台登录资格沿用既有账号绑定约束。
|
||||
|
||||
| 错误 | HTTP | 处理 |
|
||||
| --- | --- | --- |
|
||||
| UNAUTHENTICATED | 401 | 登录有效控制台账号 |
|
||||
| PERMISSION_DENIED | 403 | 核对唯一管理员配置、会话及同源请求 |
|
||||
| INVALID_ARGUMENT | 400 | 核对字段、能力组、固定身份及有效期 |
|
||||
| NOT_FOUND | 404 | 核对命名空间/凭据是否存在 |
|
||||
| STATE_CONFLICT | 409 | 应用名称已存在、凭据已撤销或版本已变更;读取当前状态 |
|
||||
| IDEMPOTENCY_CONFLICT | 409 | 原操作内容被修改,保留原始操作核对 |
|
||||
| 数据库/网络故障 | 5xx / 网络错误 | 不假定失败,保留原操作重报 |
|
||||
|
||||
可重复运行的真实 HTTP 场景见 [test-console-applications.py](../../scripts/test-console-applications.py),标准容器入口 `make console-e2e` 同时验证此链路。基础 SDK 使用方式见 [SDK 接入](../sdk/README.md)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 执行尝试与证据查询 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. 用途与权限
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ accepted, err := api.SubmitTask(ctx, "update-object-42-operation-001", orbit.Sub
|
||||
|
||||
## 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。另一个终端执行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
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 或业务数据。
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -26,8 +26,8 @@ make release-manifest > orbit-release-manifest.json
|
||||
"workflow_schema_version": 1
|
||||
},
|
||||
"schema": {
|
||||
"current_version": 28,
|
||||
"migration_count": 28,
|
||||
"current_version": 30,
|
||||
"migration_count": 30,
|
||||
"migrations": [
|
||||
{"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
|
||||
{"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 的六个有界观测索引均满足要求。需要同时核对发布物身份时,传入保存的清单:
|
||||
@@ -79,3 +79,9 @@ ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit' \
|
||||
```
|
||||
|
||||
它与 `/metrics` 共享同一有界读取实现,但不会改变指标注册器或业务状态;遇到数据库超时会返回非零状态,调用方应保留上一次成功快照并排查数据库。
|
||||
|
||||
## 文件归档与离线核验
|
||||
|
||||
[`orbit-admin retention archive / verify-archive`](retention-jobs.md) 提供持久文件作业、原目录续跑及摘要链核验。读取命名空间需显式指定已保存策略版本,每个新批次记录独立作业审计,源记录不删除;离线核验无需数据库。
|
||||
|
||||
周期归档部署、持久卷及故障处理见[周期归档管理](archive-scheduler.md);需 Schema 31,默认关闭后台归档执行器。
|
||||
|
||||
@@ -69,7 +69,7 @@ Worker 连接不再各自直接并发发起一批数据库领取。节点采用
|
||||
|
||||
## 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` 后:
|
||||
|
||||
@@ -83,9 +83,9 @@ go run ./cmd/orbit
|
||||
凭据需要全部队列范围的 queues:manage、tasks:submit/read、workers:connect/consume。另一个终端执行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
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 场景
|
||||
|
||||
源码:[resources/main.go](../../sdk/go/examples/resources/main.go)。先完成当前迁移并启动服务,初始化带 Worker 权限且 queues:manage 范围为全部队列的专用开发凭据:
|
||||
源码:[resources/main.go](../../examples/resources/main.go)。先完成当前迁移并启动服务,初始化带 Worker 权限且 queues:manage 范围为全部队列的专用开发凭据:
|
||||
|
||||
```sh
|
||||
mkdir -p .local
|
||||
@@ -120,10 +120,10 @@ chmod 700 .local
|
||||
go run ./cmd/orbit-admin bootstrap \
|
||||
--application resources-demo --namespace default --environment development \
|
||||
--queue bootstrap --worker-access --output .local/resources.credentials.json
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/resources.credentials.json \
|
||||
go run ./examples/resources
|
||||
ORBIT_CREDENTIALS_FILE=../.local/resources.credentials.json \
|
||||
go run ./resources
|
||||
```
|
||||
|
||||
示例创建独立队列(积压 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` 用于在制定保留窗口、归档和备份恢复方案前盘点历史数据。它只执行带截止时间的统计查询,不删除、搬迁、改写数据,也不会自动判断某条记录可以安全归档。
|
||||
|
||||
|
||||
@@ -92,13 +92,13 @@ WorkerFailure 默认 Retryable=false;普通 error、panic、runtime.Goexit 和
|
||||
|
||||
## 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
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
|
||||
go run ./examples/retry
|
||||
ORBIT_CREDENTIALS_FILE=../.local/worker.credentials.json \
|
||||
go run ./retry
|
||||
```
|
||||
|
||||
输入为 `{"text":" safe retry "}`。第一次处理在产生效果前模拟明确拒绝,第二次执行真正的文本规范化;重试等待由平台持久化安排。
|
||||
|
||||
@@ -178,13 +178,13 @@ go run ./cmd/orbit-admin bootstrap \
|
||||
--application schedule-demo --namespace default --environment development \
|
||||
--queue snapshots --worker-access --schedule-access \
|
||||
--output .local/schedule.credentials.json
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/schedule.credentials.json \
|
||||
go run ./examples/schedules
|
||||
ORBIT_CREDENTIALS_FILE=../.local/schedule.credentials.json \
|
||||
go run ./schedules
|
||||
```
|
||||
|
||||
前置数据库与目录步骤见 [SDK 接入](../sdk/README.md)。源码:[完整调度示例](../../sdk/go/examples/schedules/main.go)。示例创建暂停的每秒计划,恢复后让自动 Worker 返回对象输入快照,查询两次完成,再暂停并排空。计划本身有十秒结束边界,任务与历史保留;重复运行生成独立身份,不调用外部业务系统。
|
||||
前置数据库与目录步骤见 [SDK 接入](../sdk/README.md)。源码:[完整调度示例](../../examples/schedules/main.go)。示例创建暂停的每秒计划,恢复后让自动 Worker 返回对象输入快照,查询两次完成,再暂停并排空。计划本身有十秒结束边界,任务与历史保留;重复运行生成独立身份,不调用外部业务系统。
|
||||
|
||||
```text
|
||||
source_kind=SCHEDULE fixed_version=1
|
||||
|
||||
@@ -322,6 +322,6 @@ _, err = client.ConfigureSubscription(ctx, current.ID, pauseKey, orbit.Configure
|
||||
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 不代替授权、状态机、租约及事务校验。
|
||||
|
||||
@@ -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. 存储、升级与边界
|
||||
|
||||
|
||||
@@ -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 |
|
||||
| 按计划找到触发任务,再反查来源 | [schedules](../../sdk/go/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 |
|
||||
| 并行流程完成后核对每项任务来源 | [workflow](../../sdk/go/examples/workflow/main.go) | [流程场景](../examples/workflow.md),workflow_sources_verified=3、fixed_version=v1 |
|
||||
| 单项受理、取消后读取来源 | [tasks](../../examples/tasks/main.go) | [SDK初始化](../sdk/README.md),source_kind=DIRECT |
|
||||
| 按计划找到触发任务,再反查来源 | [schedules](../../examples/schedules/main.go) | [计划场景](schedules.md#6-sdk-与完整示例),source_kind=SCHEDULE、fixed_version=1 |
|
||||
| 人工核查后创建关联新任务 | [recovery](../../examples/recovery/main.go) | [恢复场景](../examples/recovery.md),source_kind=MANUAL_RETRY、parent=原任务ID |
|
||||
| 并行流程完成后核对每项任务来源 | [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。
|
||||
|
||||
|
||||
@@ -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"}}
|
||||
```
|
||||
|
||||
有限自动重试已实现,具体分类、退避、回执和升级边界见 [安全重试](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 忽略未知响应字段;更改已有字段语义需升级契约和回归。
|
||||
|
||||
83
docs/console.md
Normal file
83
docs/console.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 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,不自动猜测结果、不替操作者补齐证据,也不把冲突重试成“最新版本”。
|
||||
|
||||
## 应用接入
|
||||
|
||||
侧栏「应用接入」提供应用创建、队列与凭据查看、Token 重新生成和完整 SDK 配置交付。配置 `ORBIT_CONSOLE_APPLICATION_ADMIN=admin` 指定唯一管理登录名;本地 Compose 已配置。其他业务页面继续使用账号原有默认范围。[功能、接口、权限和恢复说明](api/console-applications.md)。
|
||||
@@ -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历史和暂停门控。本机渠道是受控夹具,不能代替持久业务证据。
|
||||
|
||||
## 已可运行:嵌入现有 HTTP 服务
|
||||
|
||||
[完整 HTTP 工程与命令](http-service.md):业务接口同事务入队 → 后台发送 → 内嵌 Worker → WebSocket → Inbox,包含健康检查、后台错误传播、SIGTERM收尾和SIGKILL恢复。公共包接口见 [MySQL Outbox](../sdk/outbox-mysql.md)。
|
||||
|
||||
## 已可运行:业务事务与任务提交Outbox
|
||||
|
||||
- 独立模块:[task-outbox](../../examples/task-outbox/README.md),[完整前置、命令、Go方法及故障处理](task-outbox.md)。
|
||||
@@ -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执行后查询开始时间;重报原请求仍返回原任务及时间。
|
||||
- 自动失败重试单独使用retry策略,初始延迟不在每次尝试中重新计时。
|
||||
|
||||
## 已可运行:健康检查
|
||||
|
||||
- 源码:[SDK 健康检查](../../sdk/go/examples/health/main.go)。
|
||||
- 源码:[SDK 健康检查](../../examples/health/main.go)。
|
||||
- 前置:已迁移的 MySQL 与已启动的基础服务。
|
||||
- 操作与预期输出:[SDK 接入说明](../sdk/README.md)。
|
||||
- 异常:数据库不可用或迁移记录异常时 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)。
|
||||
- 输入:对象引用、格式与精确整数;提交两次同一身份,再查询、分页并带版本取消。
|
||||
- 预期:重复请求返回同一任务,最终 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 凭据、已启动服务和当前迁移;标准隔离回归会自动运行本例。
|
||||
- 输入与步骤:提交带稳定操作键的两行 CSV 快照 → Worker 注册 `file.convert.csv-json` v1 → 领取、授权并转换 → 查询 JSON 结果 → 排空。
|
||||
- 预期:`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)。
|
||||
- 输入与步骤:提交 `{"text":" Hello Orbit "}` → 注册/容量 → 领取 → 开始授权 → 实际处理 → 确认结果 → 查询 → 排空。
|
||||
- 预期:SUCCEEDED、一次尝试、结果 `{"text":"HELLO ORBIT"}`。
|
||||
@@ -68,7 +74,7 @@
|
||||
|
||||
## 已可运行:自动 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 并等待排空。
|
||||
- 预期:三条规范化结果、started=3、confirmed=3、running=0;输入与结果均由实际代码处理。
|
||||
- 异常:自动心跳/续租与原身份重连;普通错误/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 权限。
|
||||
- 步骤:创建模拟任务 → 开始授权 → 保存 v1 结果 → 用另一消息身份报告 v2 → 查询尝试/证据列表与详情 → 核对任务仍保留 v1。
|
||||
- 预期: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 权限的专用凭据。
|
||||
- 步骤:第一次在产生效果前模拟明确拒绝 → 平台持久化退避 → 第二次处理文本 → 查询两次尝试 → 排空。
|
||||
- 预期: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 的专用开发凭据。
|
||||
- 步骤:创建暂停的有限每秒计划 → 恢复 → 自动 Worker 返回输入快照 → 查到至少两次任务成功 → 暂停并排空。
|
||||
- 预期:不同 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 权限。
|
||||
- 步骤:提交并开始受控任务 → 保存取消意图 → 处理器遵守 context 停止 → 明确确认本次无未结清效果 → 查询最终状态并排空。
|
||||
- 预期: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 权限。
|
||||
- 步骤:创建并暂停积压上限为 2 的队列 → 第三个任务被 429 拒绝 → 满额下核对第一条原回执 → 取消释放空间 → 用被拒原身份再提交 → 恢复并按每秒 1 次开始执行。
|
||||
- 预期: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。
|
||||
- 步骤:应用上限设为 1 → first 受理 → second 全局满额 → 取消 first → second 以原受理键成功 → 取消收尾并按 CAS 恢复配置。
|
||||
- 预期: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。
|
||||
- 步骤:创建 → 同键核对 → 暂停 → 恢复 → 查询与分页 → 最终暂停。
|
||||
- 预期: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_replayed=true、same_receipt=true、pending=0、subscription_enabled=false。
|
||||
- 范围:本例只做读取验证,不写业务数据;[自动消费器](../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。
|
||||
- 异常:逐项区分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关联新任务 → 执行成功 → 查询原任务及操作审计。
|
||||
- 预期:原任务FAILED、新任务SUCCEEDED,原尝试和审计保留;管理凭据显式--recovery-access。
|
||||
- 异常:真实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。
|
||||
- 范围:普通提交/读取/取消权限;不调用外部系统、不启动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
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/batch.credentials.json \
|
||||
go run ./examples/batch
|
||||
ORBIT_CREDENTIALS_FILE=../.local/batch.credentials.json \
|
||||
go run ./batch
|
||||
```
|
||||
|
||||
也接受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连接/消费权限,队列须有可用额度。仓库根目录执行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
|
||||
go run ./examples/delayed-task
|
||||
ORBIT_CREDENTIALS_FILE=../.local/worker.credentials.json \
|
||||
go run ./delayed-task
|
||||
```
|
||||
|
||||
也可用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)。
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
|
||||
先按 [订阅管理示例](subscriptions.md) 建立隔离示例队列和带 --event-access 的凭据,启动 Orbit。凭据需要 tasks:submit、tasks:cancel、events:manage/read/consume 及该队列范围;订阅绑定创建凭据,不能临时换凭据消费。
|
||||
|
||||
从 sdk/go 目录运行:
|
||||
从仓库根目录进入 examples 后运行:
|
||||
|
||||
```sh
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/event.credentials.json \
|
||||
go run ./examples/event-protocol
|
||||
ORBIT_CREDENTIALS_FILE=../.local/event.credentials.json \
|
||||
go run ./event-protocol
|
||||
```
|
||||
|
||||
也可以传 ORBIT_TOKEN 与 ORBIT_QUEUE_ID,环境变量覆盖凭据文件对应值。每次运行新建随机订阅及任务身份,全部流程最多 15 秒。本例是短流程,两个连接均短于一次心跳周期;长期运行必须用自动消费器或主动 Ping。
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
需要当前迁移、已启动 Orbit 服务,以及具备任务提交/读取和 Worker 消费权限的专用凭据。凭据文件应只保存在本地受保护目录:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
|
||||
go run ./examples/file-conversion
|
||||
ORBIT_CREDENTIALS_FILE=../.local/worker.credentials.json \
|
||||
go run ./file-conversion
|
||||
```
|
||||
|
||||
## 调用链与预期
|
||||
@@ -50,8 +50,8 @@ input=records.csv output=records.json rows=2 state=SUCCEEDED worker_drained=true
|
||||
单元转换校验:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
GOWORK=off go test ./examples/file-conversion
|
||||
cd examples
|
||||
GOWORK=off go test ./file-conversion
|
||||
```
|
||||
|
||||
`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
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/recovery.credentials.json \
|
||||
go run ./examples/recovery
|
||||
ORBIT_CREDENTIALS_FILE=../.local/recovery.credentials.json \
|
||||
go run ./recovery
|
||||
```
|
||||
|
||||
演示凭据为了完整运行同时包含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
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/subscriptions.credentials.json \
|
||||
go run ./examples/subscriptions
|
||||
ORBIT_CREDENTIALS_FILE=../.local/subscriptions.credentials.json \
|
||||
go run ./subscriptions
|
||||
```
|
||||
|
||||
也可设置 ORBIT_TOKEN 和 ORBIT_QUEUE_ID,环境值优先于凭据文件。生产接入使用 HTTPS;不要提交凭据文件或在命令行打印秘密。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 1. 数据与状态
|
||||
|
||||
[中文模型](../../examples/task-outbox/internal/outbox/model.go)和[SQL](../../examples/task-outbox/internal/outbox/schema.sql)包含2张业务表、22个字段,全部有中文说明,外键为零,不进入Orbit平台迁移。
|
||||
[中文模型](../../examples/task-outbox/internal/outbox/model.go)和[SQL](../../examples/task-outbox/internal/outbox/schema.sql)与[公共 Outbox SQL](../../adapters/outbox/mysql/schema.sql)合计包含2张业务表、22个字段,全部有中文说明,外键为零,不进入Orbit平台迁移。
|
||||
|
||||
| 表 | 职责与关键约束 |
|
||||
| --- | --- |
|
||||
@@ -103,7 +103,7 @@ GOWORK=off go run ./cmd/sender status "$SENDER_OPERATION_ID"
|
||||
|
||||
## 4. Go接入接口
|
||||
|
||||
以下方法属于示例的内部业务适配包,完整源码见[store.go](../../examples/task-outbox/internal/outbox/store.go)及[sender.go](../../examples/task-outbox/internal/outbox/sender.go)。接入自己的业务模块时保留事务顺序与协议,替换sender_jobs为实际业务表;核心Orbit客户端仍使用独立SDK。
|
||||
发送与租约实现已抽至[公共适配包](../sdk/outbox-mysql.md)。以下方法是本示例的业务封装,完整源码见[store.go](../../examples/task-outbox/internal/outbox/store.go)及[sender.go](../../adapters/outbox/mysql/sender.go)。接入自己的业务模块时保留事务顺序与协议,替换sender_jobs为实际业务表;核心Orbit客户端仍使用独立SDK。
|
||||
|
||||
```go
|
||||
func Open(ctx context.Context, dsn string) (*sql.DB, error)
|
||||
|
||||
@@ -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服务。从仓库根目录执行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/development.credentials.json \
|
||||
go run ./examples/task-query
|
||||
ORBIT_CREDENTIALS_FILE=../.local/development.credentials.json \
|
||||
go run ./task-query
|
||||
```
|
||||
|
||||
也可使用ORBIT_TOKEN与ORBIT_QUEUE_ID,环境值优先。无需额外管理权限,凭据秘密不输出。队列应有至少三个未终结任务空位;每次运行的处理器与业务键带独立标识,避免已有任务干扰。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
本例运行真实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. 准备与运行
|
||||
|
||||
@@ -15,11 +15,11 @@ bin/orbit-admin bootstrap --application definition-demo --namespace default --en
|
||||
首次创建文件后保留,不要覆盖或打印凭据内容。该标志只增加定义读取和发布动作;生产凭据应限制到需要的命名空间及队列。
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=/tmp/orbit-definition.credentials.json \
|
||||
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不会增加。
|
||||
|
||||
@@ -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只执行一次,凭据文件存在时不会覆盖。另开终端,从仓库根目录运行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/poll.credentials.json \
|
||||
go run ./examples/workflow-poll
|
||||
ORBIT_CREDENTIALS_FILE=../.local/poll.credentials.json \
|
||||
go run ./workflow-poll
|
||||
```
|
||||
|
||||
程序自行创建并关闭本机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
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/workflow-query.credentials.json \
|
||||
go run ./examples/workflow-query
|
||||
ORBIT_CREDENTIALS_FILE=../.local/workflow-query.credentials.json \
|
||||
go run ./workflow-query
|
||||
```
|
||||
|
||||
查询本身只要求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接入与故障示例。夹具数据仅在本次进程内保存;真实系统需要持久证据和真实授权,不能把下面的演示核查逻辑接成任意业务的自动放行。
|
||||
|
||||
@@ -23,10 +23,10 @@ go run ./cmd/orbit
|
||||
初始化一次;原凭据文件不会被覆盖。另开终端,从仓库根目录运行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/workflow-recovery.credentials.json \
|
||||
go run ./examples/workflow-recovery
|
||||
ORBIT_CREDENTIALS_FILE=../.local/workflow-recovery.credentials.json \
|
||||
go run ./workflow-recovery
|
||||
```
|
||||
|
||||
开发凭据包含定义、启动、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. 启动
|
||||
|
||||
@@ -21,10 +21,10 @@ go run ./cmd/orbit
|
||||
初始化仅执行一次,文件存在时不会覆盖。重用该凭据运行例子无需重复bootstrap。另开终端,从仓库根目录执行:
|
||||
|
||||
```sh
|
||||
cd sdk/go
|
||||
cd examples
|
||||
ORBIT_URL=http://127.0.0.1:8080 \
|
||||
ORBIT_CREDENTIALS_FILE=../../.local/workflow.credentials.json \
|
||||
go run ./examples/workflow
|
||||
ORBIT_CREDENTIALS_FILE=../.local/workflow.credentials.json \
|
||||
go run ./workflow
|
||||
```
|
||||
|
||||
生产使用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 示例及两个独立业务模块真实集成已通过;发布环境仍需复核 |
|
||||
| 容器发布物 | `make image`;[容器发布说明](21-container-deployment.md) | native linux/arm64 与显式 linux/amd64 镜像构建已通过,支持注入版本身份并由 `/version` 核对;生产镜像标签、摘要、签名和目标编排环境仍需冻结 |
|
||||
| 发布物身份清单 | `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 |
|
||||
| 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) |
|
||||
| 四项闭环验收 | [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 个示例串跑和两个独立业务模块集成;跨版本矩阵和目标拓扑仍待发布环境 |
|
||||
| 备份恢复 | `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 构建的当前二进制已通过单节点同卷演练;主从/跨故障域待验收 |
|
||||
| 运行观测和只读快照 | `/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):租户、队列、连接、事件、索引和保存周期的参数化推导 | 已建立计算方法和演算样例;目标业务数据、硬件、拓扑和生产容量仍未验收,不能由企业数直接承诺 |
|
||||
| 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));正式标签和全协议矩阵仍未验收 |
|
||||
|
||||
发布候选只有在所有“待目标环境/未验收”项得到对应环境证据后,才能进入生产审批。当前代码和阶段回归不等同于生产高可用、容量、RPO/RTO或百万企业承诺。
|
||||
|
||||
29
docs/reports/console-applications.md
Normal file
29
docs/reports/console-applications.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 单管理员应用接入验收(2026-09-27)
|
||||
|
||||
本报告记录当时的立即换证语义;后续长期凭据和双 Token 过渡已在[增量报告](credential-overlap.md)单独说明。
|
||||
|
||||
## 交付范围
|
||||
|
||||
- 独立应用接入页:按应用/环境查看接入范围、队列与凭据,创建应用/命名空间/初始队列,选择业务能力,生成或轮换 Token,复制/下载 SDK 环境配置。
|
||||
- 通过 `ORBIT_CONSOLE_APPLICATION_ADMIN` 显式指定唯一管理员,默认关闭;仅登录 Cookie 可访问管理端,业务 Token 不授予跨应用权限。同事务核验账号及会话,写请求额外核验同源与专用请求头。
|
||||
- 新建资源、权限、审计、幂等回执同事务;轮换复用原凭据事务与 ID,任务、订阅和交付身份保留。只存秘密摘要,一次性交付明文;提交不明时保留原操作核对。
|
||||
- 没有新增数据库表或外键,没有改变业务 SDK 接口,不需要升级 SDK v0.1.1。
|
||||
|
||||
## 已完成验证
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| `make test vet build` | 七模块竞态测试、静态检查和构建通过 |
|
||||
| `TestIntegrationConsoleApplications` | 真实 MySQL:SDK 身份与错误管理员拒绝、准备未生效、并发创建一次、同键改内容冲突、重名回滚、元数据脱敏、轮换原身份、旧秘密失效、重复回执、版本冲突、退出失效、审计主体及零外键通过 |
|
||||
| 管理 HTTP 单元测试 | Cookie 必须、Bearer 单独拒绝、缺少防跨站头/异源/null Origin 拒绝、合法同源请求、未知字段拒绝、响应不可缓存通过 |
|
||||
| 准备对象严格 JSON 往返 | CREATE 与 ROTATE 的可选 ID/空能力组原样提交通过;真实 HTTP 发现的空 ID 编码问题已修复并加入回归 |
|
||||
| `scripts/test-console-applications.py` | 真实 HTTP 创建/重放/冲突,任务和订阅生成,轮换后旧 Token 401、新 Token 可读取原任务与订阅,订阅暂停收尾、退出会话失效通过 |
|
||||
| `make console-e2e` | 全新 MySQL/容器初始化,应用接入新链路、登录/权限、真实 Worker/重试/定时、保留策略、周期归档及离线校验全部通过 |
|
||||
| 本地 Compose | 更新现有 18080 环境,复用原管理员、初始业务凭据与数据库卷;新增独立 access-demo 测试应用,轮换只发生于该测试应用 |
|
||||
| 浏览器检查 | 实际账号登录,独立页面与应用详情加载、新建表单显示、权限/队列/配置展示;当前 1280×720 视口布局正常,无浏览器错误 |
|
||||
|
||||
`make integration` 已完整退出 0:覆盖真实存储、HTTP/WSS、SDK、可运行示例、独立 Inbox/Outbox/HTTP 宿主、Worker 与事件进程故障以及流程多节点暂停恢复。页面布局与表单读取通过浏览器核对;实际创建与轮换的持久化链路通过真实 HTTP 验证,不混称为浏览器点击完成全部流程。文档链接检查覆盖 196 份 Markdown,全部解析通过。
|
||||
|
||||
## 边界
|
||||
|
||||
接入管理跨应用,其他业务页面仍使用账号原有默认范围。旧 Token 原文不可查看;新 Token 交付丢失时保留原应用并重新轮换。列表每页 50 个命名空间,单个详情队列和凭据各最多 100 项并显式标记截断。当前未增加删除应用、修改权限、凭据转移、复杂角色或多应用任务总览。
|
||||
34
docs/reports/credential-overlap.md
Normal file
34
docs/reports/credential-overlap.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 长期凭据与平滑换证(2026-09-27)
|
||||
|
||||
新建应用与 bootstrap 默认长期 Token,可选有限有效期。已有 Token 支持 SET_EXPIRY 原地修改有效期,原秘密和接入配置保持不变;默认换证保留 7 天过渡,支持按配置调整,确认全部服务切换后可提前 FINALIZE。REVOKE 同时拒绝两代秘密。
|
||||
|
||||
## 实现与兼容
|
||||
|
||||
- 000032 在 orbit_credentials 增加 previous_secret_digest、previous_valid_until,CHECK 要求成对为空/非空。中文模型与 SQL 注释齐全,当前空库基线为 32,无外键。历史迁移不修改。
|
||||
- 认证在原事务共享锁下读取两代摘要及各自截止;HTTP 和 Worker/结果消费复用原认证入口。截止使用数据库时间,不依赖内存、清理任务或节点时钟。
|
||||
- 旧代实际截止不晚于其原有效期;重复轮换不会挤掉尚未切换的旧代,调整当前有效期也不会顺带延长旧代。凭据 ID、权限、任务、订阅与交付身份不变。
|
||||
- 管理操作、版本、固定过渡截止、审计及幂等回执同事务。格式 v2 新增长期和过渡语义,历史 v1 仍按原固定到期/立即轮换恢复;重放历史轮换不复活已经停用的旧代。
|
||||
- 管理端增加长期选项、调整有效期、平滑换证及结束过渡;CLI 增加 expiry/finalize、默认 --valid-for 0 和轮换 --grace 168h。没有修改 SDK 接口或 Token 格式。
|
||||
|
||||
## 验证
|
||||
|
||||
| 验证 | 证据 |
|
||||
| --- | --- |
|
||||
| 标准回归 | `make test vet build` 七模块竞态测试、静态检查和构建通过 |
|
||||
| 原 Token 改长期与在途执行 | `TestIntegrationCredentialOverlap`:运行中的任务原凭据改长期后继续授权;换证期间旧 Token 续租、完成结果、释放许可;两代认证、并发原操作去重、重复轮换冲突、结束过渡、历史重报、两代撤销及审计脱敏通过 |
|
||||
| 期限与旧代隔离 | `TestIntegrationCredentialOverlapDeadline`:不超过旧有效期、两代独立到期、到期无需清理即可拒绝、下一轮不复活更旧秘密通过;时间边界由隔离测试库明确设置,不把测试耗时当作长期运行证据 |
|
||||
| 双节点真实 WS | `TestIntegrationCredentialOverlapWebSocket`:两个 HTTP/WS 节点共享真实 MySQL;原长连接在轮换后可发控制命令,旧 Token 可跨节点重连,新 Token 同时连接;结束过渡后旧连接命令和新握手被拒绝,新连接继续可用 |
|
||||
| 提交结果未知 | `TestIntegrationCredentialCommitFaults`:COMMIT 前截断及数据库提交成功后丢确认;旧 Token 始终可用,新 Token 只在实际提交后生效,原操作核对恢复 |
|
||||
| 管理 HTTP | `scripts/test-console-applications.py`:真实 Cookie 管理、CSRF、定期改长期且原 Token 200、双 Token 过渡、提前结束、旧 Token 401、新 Token 200、历史回执不能复活旧秘密、原任务/订阅读取和退出失效通过 |
|
||||
| 容器端到端 | `make console-e2e` 全新 MySQL/Orbit、上述管理链路、原登录/Worker/重试/定时/归档验收通过 |
|
||||
| 全量集成 | `make integration` 退出 0:真实 MySQL 全部存储用例、历史结构前向升级、HTTP/WS、独立 SDK、任务/定时/流程示例、Inbox/Outbox/HTTP 宿主、Worker/结果消费者进程故障及多节点流程恢复通过 |
|
||||
| 本地升级 | 复用现有数据库卷与管理员;迁移至 32,旧 CREATE 固定对象重放命中原回执;通过有审计的 SET_EXPIRY 更新既有测试凭据,原 Token 请求 200,host.env/docker.env 字节不变,跨应用队列访问仍 404 |
|
||||
| 浏览器 | 实际登录会话读取到长期有效;新建表单默认长期,平滑换证默认 7 天,调整有效期表单正确;窄窗口换证弹窗可完整操作,未通过浏览器触发实际换证 |
|
||||
|
||||
文档链接检查覆盖 197 份 Markdown,通过;JS 语法、Python 语法及 Git diff 检查通过。涉及 Token 的变更链路在隔离真实 HTTP/MySQL 环境验证,浏览器只验证页面和表单,不把两者混称为浏览器全链路测试。
|
||||
|
||||
## 使用边界
|
||||
|
||||
调用方在截止前完成滚动更新并正常收尾在途任务。SDK 在创建客户端时读取 Token,改 `.env` 不会热更新现有客户端;须重建客户端或逐台重启服务。单实例重启仍有短暂停顿,平台不能替业务服务保证副本容量。提前结束过渡或到期会拒绝尚未切换的旧 Token,不承诺强行停用旧身份后其连接还能继续工作。
|
||||
|
||||
这次数据库升级完成且所有 Orbit 节点更新后才能启用双 Token 换证;旧版本节点不认识上一代摘要。只修改有授权的本地测试凭据为长期,迁移不会自动延长其他已有凭据。完整操作顺序见[凭据手册](../api/authentication.md)和[管理接口](../api/console-applications.md)。
|
||||
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、安全重试及到期恢复共同维护应用计数,跨命名空间与队列共享。空轮询与失败受理不消耗计数,重复回执不重复计费。
|
||||
- 额度行与认证身份分离,统一按应用额度 → 队列 → 任务/尝试 → 许可顺序取锁;后台扫描跳过忙碌额度,应用停用后仍可释放到期尝试。
|
||||
- 管理 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. 环境和实际回归
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [服务契约](../../pulse/service/batch.go):最多32项,逐项独立事务与幂等,ACCEPTED/REJECTED/UNKNOWN/NOT_ATTEMPTED四类结果。未知数据库结果或凭据失效后停止余项,上下文结束保留已确认前项。
|
||||
- [HTTP入口](../../internal/transport/httpserver/tasks.go):`POST /v1/tasks:batch`,全体正文严格解析后才受理,每项正文幂等键,总请求512 KiB。完整响应使用HTTP200,逐项标记结果。
|
||||
- [独立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) 同步维护。
|
||||
|
||||
不增加数据库模型或迁移,平台仍为35个实体、289个中文注释持久化字段;不增加外键、级联或SDK数据库依赖。
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- 取消后不延长期限、不产生后续重试;有效成功结果仍能确认,普通取消错误或失联保持未知效果。
|
||||
- 明确取消结果使用 task.finish / CANCELLED;本次确认停止不能清除此前未核实的外部效果。
|
||||
- 可选择的 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,没有新增持久化字段。
|
||||
- 开发规范明确禁止再引入外键。应用启动/readiness 核验迁移及零外键要求;任务、计划及 Worker 关联由服务端事务校验。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- [EventConsumer](../../sdk/go/event_consumer.go):自动心跳、指数退避重连、本地1–32个在途事件、成功后确认、ACK不明时原证明核对、处理器失败不确认、取消与有界收尾。
|
||||
- [类型及秘密隐藏](../../sdk/go/event_types.go):ResultEvent不给业务处理器租约秘密;Grant/Proof仍可显式序列化用于受保护的存储,普通fmt隐藏敏感数据。
|
||||
- [严格解码](../../sdk/go/event_codec.go):大小写、重复键、方向、会话、规范uint64、证明编码和事件/交付/快照关联。大整数不经float64转换。协议类WebSocket关闭按永久错误处理。
|
||||
- [协议例子](../../sdk/go/examples/event-protocol/main.go):产生取消结果、读取、ACK、重连重报、验证原确认时间、暂停订阅。它只读取结果,不执行接收方业务写入。
|
||||
- [协议例子](../../examples/event-protocol/main.go):产生取消结果、读取、ACK、重连重报、验证原确认时间、暂停订阅。它只读取结果,不执行接收方业务写入。
|
||||
- SDK保持独立模块,无新增数据库驱动、服务端包或持久化模型。本阶段不增加迁移、外键或级联删除。
|
||||
|
||||
## 2. 验证环境与入口
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- 续租、取消、结果、退避及到期恢复同步维护两类许可。UNKNOWN 释放执行许可并保留核查与未知效果,旧结果不能释放后续尝试的许可。
|
||||
- Worker 显式协商 concurrency_keys;旧 Worker 不消费有键任务,自动 SDK 默认开启。协议 Schema、SDK 精确 uint64、尝试历史与计划模板同步支持。
|
||||
- [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. 环境与命令
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- 以最多 270 秒的排序优势实现优先级加速;等待超过此优势的旧任务不被不断到来的最高级新任务持续插队。没有承诺 270 秒内完成或严格 FIFO。
|
||||
- [节点领取池](../../internal/dispatch/pool.go) 将原来的各连接并发轮询改为应用 → 队列 → 连接请求轮转。总并发 4、单应用在途 1、待处理加在途最多 128;每连接最多一个未完成请求。应用归属由认证后的数据库关系取得,不能按自报应用或不同命名空间分组。
|
||||
- 同命名空间/队列/精确能力的空探测在节点内合并一个周期,缓存最多 128 项;控制收发继续运行。所有任务状态、许可、预算和执行授权仍由数据库事务决定。
|
||||
- [可运行示例](../../sdk/go/examples/priority/main.go)、协议 Schema、接口与 SDK 文档同步更新。当前仍为 33 个实体,274 个直接声明持久化字段全部有中文注释,最终数据库外键为零。
|
||||
- [可运行示例](../../examples/priority/main.go)、协议 Schema、接口与 SDK 文档同步更新。当前仍为 33 个实体,274 个直接声明持久化字段全部有中文注释,最终数据库外键为零。
|
||||
|
||||
## 2. 环境和执行
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- 计划补跑受相同积压上限限制,保留未受理的原触发点;释放空间后按冻结漏跑策略接续。
|
||||
- 迁移 000009 增加四个队列字段并回填既有未终结任务,Go/SQL 均有中文说明,无数据库外键。
|
||||
- 源码核对 257 个直接声明的持久化字段均有中文注释;实体仍为 32 个。
|
||||
- [队列接口与完整 SDK 场景](../api/queues.md)、[resources 示例](../../sdk/go/examples/resources/main.go) 同步交付。
|
||||
- [队列接口与完整 SDK 场景](../api/queues.md)、[resources 示例](../../examples/resources/main.go) 同步交付。
|
||||
|
||||
## 2. 环境及执行结果
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- 每节点 128 条事件连接;异步领取池 4 个全局在途、同应用 1 个在途、等待加在途总量 128,按认证应用及订阅轮转。控制收发不被同连接的慢领取阻塞。连接容量仍由持久化租约校验。
|
||||
- [严格解码](../../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/go/examples/subscriptions/main.go) 已纳入统一隔离环境脚本。原始 WSS 测试辅助代码不作为正式消费 SDK 对外提供。
|
||||
- [完整管理例子](../../examples/subscriptions/main.go) 已纳入统一隔离环境脚本。原始 WSS 测试辅助代码不作为正式消费 SDK 对外提供。
|
||||
|
||||
## 2. 验证环境与结果
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- 抖动退避写入数据库 available_at;与尝试结果、许可释放、事件、结果审计及操作回执原子提交。后台回收保留含节点身份与决策的事件。
|
||||
- 结果换消息身份重报也取原已应用回执;过滤早到的证据回执,避免借用后续尝试状态。
|
||||
- 前向迁移 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. 环境与执行命令
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- 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 个直接声明的持久化字段均有中文说明。
|
||||
|
||||
完整字段、权限、状态与示例见 [接口手册](../api/schedules.md)。
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
- [独立模块](../../examples/task-outbox/README.md)只依赖独立Orbit SDK与业务侧MySQL驱动,核心SDK没有新增依赖。
|
||||
- [模型](../../examples/task-outbox/internal/outbox/model.go)与[建表](../../examples/task-outbox/internal/outbox/schema.sql):2个业务实体、22个Go/SQL中文注释字段,零外键;不改变平台35个实体和289个持久化字段。
|
||||
- [业务事务](../../examples/task-outbox/internal/outbox/store.go):业务操作与待发命令原子创建,稳定操作/提交身份、冻结请求、摘要、有限次数、租约代次及原回执;确认与业务任务关联再次同事务。
|
||||
- [发送器](../../examples/task-outbox/internal/outbox/sender.go):事务外HTTP、当前凭据核对目标、原键重报、背压退避、未知结果分类、过期接管与旧代次拒绝。
|
||||
- [发送器](../../adapters/outbox/mysql/sender.go):事务外HTTP、当前凭据核对目标、原键重报、背压退避、未知结果分类、过期接管与旧代次拒绝。
|
||||
- [CLI](../../examples/task-outbox/cmd/sender/main.go)及[完整命令/方法/恢复手册](../examples/task-outbox.md):init、enqueue、send、status和demo。
|
||||
- [Makefile](../../Makefile)和[隔离脚本](../../scripts/test-mysql.sh)纳入独立模块,使用单独业务数据库和专用平台凭据,不触碰现有开发数据库。
|
||||
|
||||
## 2. 定向数据库与故障证据
|
||||
|
||||
[数据库回归](../../examples/task-outbox/internal/outbox/store_integration_test.go)验证:
|
||||
[数据库回归](../../adapters/outbox/mysql/store_integration_test.go)验证:
|
||||
|
||||
1. 8个相同业务操作并发入库只有一条业务记录/Outbox,7次重复核对;修改正文、源引用或目标队列不能复用原操作。
|
||||
2. 实际业务INSERT失败时Outbox一起回滚;业务关联UPDATE失败时ACCEPTED一起回滚,恢复后可保存原回执。
|
||||
@@ -21,13 +21,13 @@
|
||||
4. 错误目标、损坏文档、权限拒绝、幂等冲突和无效回执停止自动发送;背压遵守Retry-After,次数耗尽和截止产生REVIEW,最后一次未知响应不能绕过上限。
|
||||
5. 实际Schema查询确认22个中文列注释、外键0,通过实际违规写入验证JSON、状态、次数和截止CHECK生效,领取查询使用ix_sender_due且无filesort;大整数9007199254740993在冻结请求与真实回执中保持精确。
|
||||
|
||||
[COMMIT故障代理](../../examples/task-outbox/internal/outbox/commit_loss_integration_test.go)在业务入库、领取和本地确认三处分别截获真实MySQL COMMIT OK包后丢弃。调用方得到ErrCommitUnconfirmed,新Store分别核对已存在业务记录、已提交租约和已确认关联,不假定COMMIT失败即回滚。
|
||||
[COMMIT故障代理](../../adapters/outbox/mysql/commit_loss_integration_test.go)在业务入库、领取和本地确认三处分别截获真实MySQL COMMIT OK包后丢弃。调用方得到ErrCommitUnconfirmed,新Store分别核对已存在业务记录、已提交租约和已确认关联,不假定COMMIT失败即回滚。
|
||||
|
||||
[HTTP回执丢失](../../examples/task-outbox/internal/outbox/http_integration_test.go)先取得真实平台受理结果,再向客户端发送不完整响应并断开;Outbox保持PENDING,原键重报后同一任务ACCEPTED、deduplicated=true、发送次数2,队列只有一项任务。错误持久化命名空间在提交前被拒绝。
|
||||
[HTTP回执丢失](../../adapters/outbox/mysql/http_integration_test.go)先取得真实平台受理结果,再向客户端发送不完整响应并断开;Outbox保持PENDING,原键重报后同一任务ACCEPTED、deduplicated=true、发送次数2,队列只有一项任务。错误持久化命名空间在提交前被拒绝。
|
||||
|
||||
[发送进程故障](../../examples/task-outbox/internal/outbox/process_integration_test.go)等待真实平台受理后、本地确认前的屏障,再SIGKILL并确认旧进程已退出。新进程等待数据库30秒租约自然到期,以原键取得原回执;最终只存在一个平台任务和一条本地业务记录。测试没有直接改时间来替代这项进程接管。
|
||||
[发送进程故障](../../adapters/outbox/mysql/process_integration_test.go)等待真实平台受理后、本地确认前的屏障,再SIGKILL并确认旧进程已退出。新进程等待数据库30秒租约自然到期,以原键取得原回执;最终只存在一个平台任务和一条本地业务记录。测试没有直接改时间来替代这项进程接管。
|
||||
|
||||
定向脚本第三轮正常退出,状态码0:Outbox集成33.009秒,其中发送进程故障31.19秒;init/demo均正常完成。随后增加的[真实CLI测试](../../examples/task-outbox/internal/outbox/cli_integration_test.go)进入本轮全量回归,验证编译后的enqueue重复、status、send及正常信号停止。
|
||||
定向脚本第三轮正常退出,状态码0:Outbox集成33.009秒,其中发送进程故障31.19秒;init/demo均正常完成。随后增加的[真实CLI测试](../../adapters/outbox/mysql/cli_integration_test.go)进入本轮全量回归,验证编译后的enqueue重复、status、send及正常信号停止。
|
||||
|
||||
## 3. 发现与修正
|
||||
|
||||
@@ -73,3 +73,5 @@ make integration
|
||||
- REVIEW保留原数据,由接入方带证据和审计的管理流程处置;本示例未实现通用业务审核系统或跨业务ORM适配。
|
||||
- 核心SDK、平台Schema和执行协议均未改变;新增业务示例不是已发布的兼容性SDK包。
|
||||
- 流程启动与信号Outbox类型需随Weave API实现;P6继续容量、数据库切换、备份恢复及运维验证。当前正确性与进程恢复证据不等同于生产高可用或百万规模容量结论。
|
||||
|
||||
2026-09-24 源码组织调整:发送核心及原故障回归迁至独立 adapters/outbox/mysql 模块,业务示例调用公共包;Outbox Record/SQL见[公共模型](../../adapters/outbox/mysql/model.go)与[表结构](../../adapters/outbox/mysql/schema.sql),sender_jobs 保留在旧业务示例。上述历史验证日期不变,新增验证另见[接入适配报告](outbox-http-integration.md)。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- [查询存储](../../internal/storage/mysql/task_query.go):固定四种索引分支、参数化SQL、创建时间/ID游标分页,v2摘要绑定全部筛选,旧游标只兼容原队列/状态查询。
|
||||
- [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个持久化字段。
|
||||
- [接口手册](../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. 真实数据库验证
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [服务契约](../../pulse/service/recovery.go):显式历史执行结束/安全重试声明、原因、证据、版本及预算校验。
|
||||
- [数据库事务](../../internal/storage/mysql/task_recovery.go):NEEDS_REVIEW核查为SUCCEEDED/FAILED,原尝试保留;独立FAILED/CANCELLED任务生成retry_of关联后继,原任务不改写。
|
||||
- [审计查询](../../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个持久化字段,无新增字段,无外键/级联。
|
||||
|
||||
## 2. 已覆盖的数据库与协议场景
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [服务模型](../../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和队列范围,拒绝查询参数。
|
||||
- [独立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个中文注释字段。新只读结构的字段也均有中文说明。
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| 立即、相对延迟、指定时间提交 | 立即受理、数据库时间计算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回执丢失及原键补交 |
|
||||
| 幂等及不可变参数/策略 | 任务、身份回执、事件与审计同事务;同语义原回执、变更冲突;[任务用例](../../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)保留退避、次数和截止,过期不补发 |
|
||||
| 等待/运行取消 | 持久化意图、预占阻止开始、运行收集结果、未知效果仍核查;[取消事务](../../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) |
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
3. 已知失败步骤与并行信号等待 → RearmWorkflowWait第二代,原截止不变 → 旧代次信号被拒绝 → RetryWorkflowStep只保留一次剩余尝试 → 解除核查 → 新任务和新代次信号完成流程。
|
||||
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个直接持久化字段有中文注释。
|
||||
- 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轮,激活与业务轮分别计数,原历史可读。
|
||||
|
||||
源码:[来源存储](../../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 结果不代表任意网络文件系统或目标生产跨故障域验收。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user