Files
qinglong/docs/adr/ADR-0378-local-run-attempt-log-retention-and-tombstones.md
T

8.4 KiB
Raw Blame History

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。每条记录精确绑定:

  • projectIdrunIdattemptIdlogArtifactId
  • executorTypefinishedAtMseligibleAtMsretiredAtMs
  • deleted | already_absent
  • 删除前观测到的 byteLengthtrue | 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-retention0088-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 packagesingleSourcePackages=[]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。