feat(ql3): add local run log retention

This commit is contained in:
whyour
2026-08-12 02:57:43 +08:00
parent 308aa75d89
commit 2bfa8ca279
50 changed files with 2752 additions and 85 deletions
+17
View File
@@ -11,6 +11,23 @@
最新增量证据(2026-08-12):
- D-290/ADR-0378(已接受)
Local Run Attempt 日志 retention 已形成真实纵向切片:Runtime Core 增加精确 identity、canonical SHA-256 的 immutable
retirement record、容量压力策略、有界 page/delete budget 与 durable cursor;日志读取在存储前检查 tombstone,并在 missing 后二次
检查,授权调用方稳定获得 `410 retired`,未授权/不存在继续在 metadata 前遮蔽为 404。SQLite 追加 0087/0088,将 Local contract
升至 v44,候选严格排除 lost、legacy owner、非 Local executor、未终态 Run/Attempt、未完成 receipt 与既有 tombstone。私有文件端只
删除 0700 owner-only shard 中 0600、单 hard-link 的 canonical 日志及唯一 truncation fact,执行 unlink→helper unlink→directory
fsync→tombstoneunlink 后崩溃以下一轮 `already_absent` 收敛。Edge 为 7 天/压力 24 小时、64 MiB 阈值、page 4/delete 2
Standalone 为 30 天/压力 24 小时、256 MiB、page 16/delete 8。sweep 复用现有 completion cleanup cadence,不新增 package、依赖、
timer、listener、连接、watcher 或 cache。Runtime Core 定向 9/9、Local SQLite 220/220、文件与 control 定向 10/10、应用真实删除+
tombstone+读取闭环通过。最终验收为 Runtime Core 498/498、Local SQLite 220/220、Local Execution 39/39、Local API
45/45、Local Application 46 pass/4 skip、Local Admin 91/91、Local Owner CLI 157 pass/5 skip、Cluster Control
216 pass/2 skip;完整 18-package 门退出 0backend 1,163 pass/2 skip/0 fail。package/dependency/Edge import
审计与 14 档 Profile artifact 全部 compatible,且没有 single-source/shallow package。真实 arm64 Local image 以固定 repo
digest 完成 Edge/Standalone preflight 和 rollout,均观测 SQLite contract v44;镜像、静态审计和 CI compatibility label
已统一为 v44。PostgreSQL 18.4 arm64 HA 回归通过 112 gates、timeline 1→2,独立证据审计无 finding。Cluster 仅冻结可注入的
410 语义;
PostgreSQL 多副本 claim/backoff、ETag/version 条件 S3 删除与 HA/MinIO 门由 D-291 独立完成。
- D-289/ADR-0377(已接受)
Local/Cluster 已增加同构 `GET /api/v3/projects/{projectId}/runs/{runId}/attempts/{attemptId}/log`
`run.log.read`/`artifact.read`。请求只接受 Project/Run/Attempt identity 与 offset/length,不接受 Artifact ID、路径、URI、bucket
@@ -4,6 +4,9 @@
- 日期:2026-07-18
- 关联:QL-RFC-0001、ADR-0007、ADR-0021、ADR-0024、ADR-0025
> 2026-08-12Local Run Attempt 日志 retention、durable tombstone、压力档位与 lifecycle 接线已由
> ADR-0378 接受;本 ADR 仍保持 Proposed,仅表示其余通用 Artifact quota/跨 Profile 扩展尚未整体关闭。
## 上下文
ADR-0024 已建立 Attempt-scoped opaque Artifact 和 direct-file durable output,但未限制单次运行能够写入的字节数,也没有终态清理证据。对小型路由设备,这意味着一个失控脚本可以填满系统盘;只按 timer 删除目录又可能误删仍在运行、等待 completion receipt 或无法证明进程退出的日志。standalone/cluster 虽然容量更高,同样需要可审计的配额和生命周期语义。
@@ -4,6 +4,9 @@
- 日期:2026-07-19
- 关联:QL-RFC-0001、ADR-0024、ADR-0026
> 2026-08-12Local/Cluster 生产读取接线由 ADR-0377 接受,Local retention 二次检查与 durable 410
> 由 ADR-0378 接受;Cluster retention 删除实现仍属于 D-291,本 ADR 的其余跨 adapter 扩展继续保持 Proposed。
## 上下文
2.x 日志 API 由全局登录或 Open API scope 保护,读取端仍接受 `path + file`,无法表达 Project 所有权、`artifact.read` 策略和 opaque Artifact identity。3.0 已把 LocalProcess 日志绑定到 `(projectId, runId, attemptId, logArtifactId)`,并用 retention tombstone 与 canonical truncation fact 保存清理和截断证据,但尚无稳定读取语义。
@@ -0,0 +1,128 @@
# ADR-0378Local Run Attempt 日志有界保留与 durable tombstone
- 状态:Accepted
- 日期:2026-08-12
- 关联 RFCQL-RFC-0001 D-290
- 前置决策:ADR-0026、ADR-0027、ADR-0377
## 上下文
ADR-0377 已让 Local/Cluster 能在 Project、Run、Attempt、executor 与 Artifact identity 全部可信后读取日志,但故意没有把缺失文件解释成 retention。若没有 durable tombstone,文件被清理、文件损坏、错误路径和未发布 Artifact 对读取方都是同一个 `missing`;若只在删除前写标记,又会把仍存在的数据错误暴露为已清理。
Local Profile 还必须覆盖两类差异很大的设备:128 MiB/64 PID 级路由设备不能承担目录全扫、每任务定时器或无界删除;Standalone 可以提高吞吐,但仍应复用单连接和既有 lifecycle。Cluster 的 PostgreSQL 多副本 claim、S3 条件删除和外部网络失败语义与本机 unlink 不同,不能为了表面复用而塞进同一个事务或 adapter。
## 决策
### 1. 共享不可变证据,不共享删除实现
`runtime-core` 在既有 Run 能力内增加 `qinglong/run-attempt-log-retirement@v1`,不创建新 package。每条记录精确绑定:
- `projectId``runId``attemptId``logArtifactId`
- `executorType``finishedAtMs``eligibleAtMs``retiredAtMs`
- `deleted | already_absent`
- 删除前观测到的 `byteLength``true | false | unknown` truncation
- 覆盖全部语义字段的 canonical SHA-256 `recordDigest`
读取 repository 必须重新计算摘要并验证精确身份;持久化行被原地修改时失败关闭。`already_absent` 的 byteLength 固定为 0,不伪造回收字节。
### 2. 读取与删除竞态
读取仍先完成 HTTP authentication、Policy、durable audit、credential confirmation 和 Run/Attempt metadata 验证。获得 canonical Artifact identity 后:
1. 在访问文件/对象前检查 tombstone
2. 已 retired 直接返回 `410 artifact retired`,不触碰存储;
3. active 时执行原有 Range read
4. 存储返回 missing 后再次检查 tombstone
5. 二次检查发现 retired 返回 410,否则保持 503 unavailable/pending 语义。
打开文件后发生 unlink 的并发读取可完成该已打开快照;这是以安全打开的 inode 为线性化点。删除完成但 tombstone 尚未提交的极短崩溃窗口由下次 `already_absent` sweep 收敛,不能预写“已删除”tombstone。
对外 `retired` 只包含 Project/Run/Attempt、retired time、原 byte length 与 truncation,不返回路径、Artifact ID、bucket 或 key。Policy deny、不存在和跨 Project 继续在此之前遮蔽为 404。
### 3. Local SQLite 44 号契约
追加 `0087-run-attempt-log-retention``0088-capability-v44`
- `QingLong3RunAttemptLogArtifactTombstones` 保存不可变精确证据;
- `QingLong3RunAttemptLogRetentionState` 保存唯一 Local maintenance cursor
- `RunAttempts` 增加局部候选索引;
- runtime readiness 仍只读取冻结 manifest,不加载可执行 DDL。
候选必须同时满足:
- Run `executionOwner=runtime`
- Run 和 Attempt 都是 `succeeded | failed | cancelled | timed_out`,明确排除 `lost`
- Attempt 为 `local_process` 且绑定 canonical `local-*` 日志;
- Run/Attempt 都有终态时间且早于当前 retention cutoff
- 不存在任何 completion receipt journal
- 不存在同 Attempt 或 Artifact 的 tombstone。
删除后写 tombstone 的事务会重新验证上述持久化事实。若 receipt 或状态在文件删除后改变,写入失败,后续 sweep 以 absent 重新收敛,不能绕过 completion 协议。
### 4. 私有文件删除顺序
Local store 只接受:
- 绝对且非文件系统根的 Artifact root
- 当前 owner、模式 0700、非 symlink 的 root/shard
- 当前 owner、模式 0600、普通文件、单 hard link、最大 1 GiB 的主日志;
- 唯一允许的 `.<artifact>.log.truncated.json` helper,且其 identity 与主记录完全一致。
顺序固定为:验证目录和 fact → 打开并验证主日志 inode → 删除主日志 → 删除允许的 fact → fsync shard directory → SQLite 写 tombstone。禁止递归删除、扫描任意目录、跟随 symlink 或清理未知 helper。若主日志已不存在但 exact fact 仍在,只删除该 fact、fsync 并记录 `already_absent`
### 5. Edge 与 Standalone 资源档位
每次 sweep 先用 `statfs` 采样可用空间,根目录尚未创建时只检查最近存在父目录,不主动创建路径。
| Profile | 正常保留 | 压力保留 | 压力阈值 | page | 最大删除 |
| --- | ---: | ---: | ---: | ---: | ---: |
| Edge | 7 天 | 24 小时 | 64 MiB | 4 | 2 |
| Standalone | 30 天 | 24 小时 | 256 MiB | 16 | 8 |
cursor 持久化到 SQLite;失败候选会被本轮越过,并在游标回绕后重试,避免一个坏文件永久阻塞队首。容量证据以十进制字符串进入 lifecycle summary,避免 JSON 对 BigInt 失败。
### 6. 不新增常驻资源
Artifact sweep 复用 `LocalExecutionControlLifecycle` 已有 completion cleanup cadence:先清 completion receipt,再做一次有界 Artifact sweep。它不新增 timer、listener、SQLite connection、watcher、cache 或 background threadstop drain 不额外执行 Artifact 删除,避免扩大关停超时。
### 7. Cluster 留给 D-291
Cluster 路由和共享读取服务已经能注入 retention state 并映射 410,但 D-290 不授予 S3 DeleteObject 权限、不新增 PostgreSQL tombstone/claim,也不宣称 Cluster retention 已完成。D-291 必须独立解决:
- 多副本 durable claim、lease/backoff 与 bounded scheduler budget
- validated HEAD 后基于 ETag/version 的条件删除;
- PostgreSQL 事务不得跨 S3 网络调用;
- delete 成功/对象已不存在后的 exact tombstone finalize
- MinIO/S3 失败矩阵与 PostgreSQL HA failover。
## 被否决的替代方案
1. **删除前写 tombstone**:可能把仍存在的数据声明为已删除,拒绝。
2. **把 missing 直接映射 410**:无法区分损坏、误配和 retention,拒绝。
3. **递归扫描 Artifact root**:I/O 无界且目录内容成为隐式 authority,拒绝。
4. **每个 Run/Attempt 启动 retention timer**:常驻资源随任务数增长,拒绝。
5. **Local/Cluster 共用一个删除事务 adapter**:会把外部 S3 调用放入数据库事务并模糊多副本 ownership,拒绝。
6. **新建 retention 微包**:现有 runtime-core、local-sqlite、local-execution ownership 已足够,拒绝。
## 验收
1. 共享 contract 覆盖 digest tamper、精确 identity、压力策略、删除预算和 durable cursor
2. 读取覆盖 tombstone-before-storage 与 missing-after-storage 二次检查;
3. SQLite migration、typed schema、readiness、candidate、receipt fence、replay/tamper 全部通过;
4. 文件 adapter 覆盖权限、symlink/hard-link、fact drift、unlink-before-tombstone 与 statfs
5. 完整 Local application 证明启动 sweep 删除真实文件、写 tombstone,并在产品读取前返回 retired;
6. Runtime Core、Local SQLite/Execution/API/Application/Admin/Owner CLI、Cluster Control 与完整 18-package/backend/boundary/Profile/Local image 门全部通过后才改为 Accepted。
2026-08-12 验收完成:Runtime Core 498/498、Local SQLite 220/220、Local Execution 39/39、Local API
45/45、Local Application 46 pass/4 skip、Local Admin 91/91、Local Owner CLI 157 pass/5 skip、Cluster
Control 216 pass/2 skip;完整 18-package 门退出 0backend 1,163 pass/2 skip/0 fail。package boundary 保持
18 个 workspace package`singleSourcePackages=[]``shallowSourcePackages=[]`dependency 与 Edge import 审计
均 compatible。14 个 Local Profile artifact 全部在文件数、体积与 RSS 预算内,最小 Edge 为 2,450,378 bytes
Edge Application 为 3,482,708 bytes,未引入 Cluster/PostgreSQL/AWS SDK 闭包。
真实 arm64 Local image 以 repo digest
`sha256:73a93094ebc53effbbe619e29ea866e59872211bf840db86509c011389ed10b8` 完成 Edge/Standalone
Compose preflight,均观测 SQLite contract v44;两档 rollout 的备份、恢复、响应丢失重放、证据收集与优雅清理
全部 compatible。PostgreSQL 18.4 arm64 HA 回归通过 112 gates、timeline `1→2`,私有报告 SHA-256 为
`6a205d8bc596097f91a900d7cdabef21c6ff3ad61152e8dfb31291cb8a356b12`,独立审计
`compatible=true/findings=[]`。发布镜像、静态审计和 CI 的 SQLite compatibility label 已统一为 v44。
+1 -1
View File
@@ -12,7 +12,7 @@
},
{
"kind": "ordered_ledger",
"maxDirectSourceFiles": 87,
"maxDirectSourceFiles": 89,
"path": "packages/ql3-local-sqlite/src/migrations",
"rationale": "SQLite migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
}