feat: schedule persistent archive jobs with console management

This commit is contained in:
2026-09-23 11:29:32 +08:00
parent 6eb429e21f
commit 0140c78ea7
32 changed files with 2077 additions and 20 deletions

View File

@@ -64,3 +64,5 @@ P1 业务接口已遵守下述约定,后续接口继续沿用:
实现时必须补齐:用途与权限、HTTP 方法和路径、请求头、字段类型/必填/默认值/限制、完整请求与响应、错误码、幂等与并发规则、超时/重试/取消语义、SDK 对应方法、可运行示例、兼容版本和已覆盖的回归场景。
[流程实例筛选分页与权限](workflow-queries.md)已接入,固定筛选沿游标分页,覆盖未执行分支的全部队列权限。
- [周期归档与作业管理](archive-scheduler.md)

View 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 个目录项,包含未完成、失败与已关闭作业;超过边界停止。并发同卷执行受全卷文件锁约束。它不等价于文件系统物理空间配额,不包含块分配/元数据开销,也不能约束绕过平台直接写目录的外部进程。生产使用独立持久卷,并监测实际磁盘余量。暂不提供文件删除、自动迁移介质、下载正文或对象存储接口。

View File

@@ -47,7 +47,7 @@ ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit' \
成功时输出类似:
```json
{"schema_version":30,"migration_count":30,"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 的六个有界观测索引均满足要求。需要同时核对发布物身份时,传入保存的清单:
@@ -83,3 +83,5 @@ ORBIT_MYSQL_DSN='user:password@tcp(127.0.0.1:3306)/orbit' \
## 文件归档与离线核验
[`orbit-admin retention archive / verify-archive`](retention-jobs.md) 提供持久文件作业、原目录续跑及摘要链核验。读取命名空间需显式指定已保存策略版本,每个新批次记录独立作业审计,源记录不删除;离线核验无需数据库。
周期归档部署、持久卷及故障处理见[周期归档管理](archive-scheduler.md);需 Schema 31,默认关闭后台归档执行器。

View File

@@ -2,7 +2,7 @@
`orbit-admin retention archive` 自动按已保存的保留策略分批导出,并把每个批次可靠写入本地持久目录。它面向持有数据库运维权限的操作者,复用控制台导出的候选保护和版本核对,记录 `archive:<job_id>` 审计身份;不需要控制台 Cookie,也不向业务 SDK 增加管理权限。数据库业务记录始终保留。
本次提供的是可重复运行的有限批次作业执行器。周期触发、控制台作业列表、对象存储和清理执行器尚未接入;不能把它描述为已部署的定时归档服务。
本页介绍可独立运行的有限批次 CLI 执行器。周期触发、控制台作业列表与人工重试见[周期归档管理](archive-scheduler.md);对象存储和源数据清理尚未接入。
## 配置与启动
@@ -111,3 +111,5 @@ docker run --rm --network none \
当前文件执行器支持 Linux/macOS 本地 POSIX 文件锁、硬链接原子发布及文件/目录同步语义;其他系统显式拒绝。不同节点独立磁盘不共享进度;网络共享文件系统必须验证上述语义后才能使用,当前没有跨节点自动接管承诺。
容量上限覆盖一个作业的清单和已发布批次,不是整个卷的配额;还需预留一个临时批次空间。已有档案不会自动删除腾空间,不会自动生成新目录重试。每个作业最多 10000 批。该归档不是数据库备份,也不构成源数据删除授权。
周期触发、控制台作业列表与人工重试已由 Schema 31 增加,见[周期归档管理](archive-scheduler.md)。本页 CLI 仍可独立使用;独立 CLI 作业不会自动注册到管理列表。