Files
qinglong/docs/adr/ADR-0115-bounded-worker-file-log-artifact-and-output-ownership.md
T

5.4 KiB
Raw Blame History

ADR-0115:有界 Worker 文件 Log Artifact 与 Output Ownership

  • 状态:Accepted
  • 日期:2026-07-23
  • 关联 RFCQL-RFC-0001 D-24、D-26、D-71、D-110、D-112、D-114
  • 关联 ADRADR-0024、ADR-0026、ADR-0071、ADR-0111、ADR-0113

背景

ADR-0113 只把 logArtifactId 送到了 Executor launch,没有具体 Worker writer,也没有定义 materializer dispose 与异步进程输出之间的所有权。若在 Processor.launch 的 finally 中 关闭 writerExecutor.start 返回后仍在运行的进程会丢失日志;若在 spawn barrier 前交接, journal 写失败又会留下无 owner descriptor。隐式无限文件对路由设备还会直接耗尽系统盘。

Worker 同时需要适配小型路由器和普通集群节点,但这不应产生两个执行协议、额外 package 或常驻清理 timer。

决策

1. 单一、opaque、offer-scoped 日志身份

WorkerFileLogArtifactAllocator 在现有 @qinglong/worker-runtime package 内实现,不新增 package 或第三方依赖。ID 使用 domain-separated SHA-256 从 (projectId, runId, attemptId, offerId) 派生为 wlog- 加 30 个十六进制字符,总长 35。 输入只用于 digest,不进入目录名、日志正文或诊断。相同 offer 重放得到同一 ID;新的 offer 得到新 ID,避免租约重新分配后的两个 Worker 向同一对象身份竞争上传。

日志路径只能从 opaque ID 派生为固定两字符 shard 和 .log 文件。root/shard 必须是当前 进程 owner 的普通目录并强制 0700;文件使用 O_CREAT|O_APPEND|O_NOFOLLOW0600 打开后复验 ordinary file、owner、单 hard-link、path 与 fd 的 device/inode identity,以及 目录在打开窗口内未被替换。非法 symlink、hard-link、owner 或 identity 全部 fail closed。

2. Profile 显式容量与两层硬上限

allocator 不允许隐式无限策略:

Worker profile 单 Attempt 上限 建立前最小剩余空间 单 write 上限
Edge 4 MiB 32 MiB 1 MiB
Node 64 MiB 256 MiB 1 MiB

创建 shard/file 前必须证明 available >= minimumFree + maximumAttempt。capacity probe 异常、 负数或非 bigint 视为不可用。writer 串行化进程内写入、立即复制 caller buffer,使用 append 保留重启前已接受 prefix;达到剩余 quota 时只写可接受 prefix,随后返回稳定 quota_exceeded。oversized/非法 stream/time/chunk 在写入前拒绝。跨进程唯一 writer 继续由 ADR-0110/0111 的单 journal owner 保证;不能把 allocator 当作第二个分布式锁。

writer close 幂等,等待所有已接受写入、执行 fdatasync 后关闭 descriptor。它不删除内容, 也不通过 timer 清理。

3. durable barrier 后一次性交接 ownership

Artifact preparation exact shape 为 logArtifactId + takeOutput + release

  1. Secret 全部闭合后才 prepare
  2. Processor 校验环境和日志 ID
  3. inbox CAS 写入 durable launching barrier
  4. 只调用一次 takeOutput(),复验 sink ID 与 materialized ID 完全相同;
  5. 调用 Executor.start 时 ownership 转移给 Executor
  6. materializer finally 仍释放 Secret 和 preparation,但 handed-off release 不关闭 writer。

Executor 对所有已知 terminal path 负责关闭 writer。tagged rejected 明确证明未 spawn Processor 再执行一次防御性幂等 close;抛错或非法结果属于 unknown spawnProcessor 禁止 关闭,交给 recovery/exact process identity 后续裁决。这样 running ACK、journal、writer 与 未来上传都只有一个 logArtifactId

4. 后续闭环

ADR-0116 已复用受审 local-process 实现具体 POSIX Executor、launcher truncation fact 读取、 流式 upload ACK 校验和 upload-before-completion 协调,但中央对象存储/认证 HTTPS adapter、 PostgreSQL completion、range API、terminal retention、失租/cancellation/timeout 与生产 headless 组合仍默认关闭。本地 spool 在这些闭环前不得删除。

被否决的替代方案

  1. writer 作为新 package:没有独立部署或供应链边界,只制造 package 碎片。
  2. Executor 自己选择日志路径/ID:控制面 Artifact identity 与实际字节分叉。
  3. 在 materializer dispose 中无条件 close:会截断已经交给异步 Executor 的输出。
  4. spawn barrier 前 handoffbarrier CAS 失败会产生无 owner writer。
  5. unknown spawn 时 Processor close:可能关闭正在运行进程的唯一日志出口。
  6. Edge/Node 两套协议或实现:只需显式策略不同,不应复制状态机。
  7. 后台 timer 做 quota/cleanup:增加低配设备常驻资源,且 timer 不能证明进程终态。

验收证据

  1. 相同 authority ID 稳定,新 offer ID 不同且不暴露输入。
  2. root/shard/file 权限分别为 0700/0700/0600symlink target fail closed。
  3. reopen 使用同一 ID 并 append 已接受 prefix,不 truncate。
  4. 总 quota 只保存剩余 prefix并返回 quota_exceededoversized chunk 零写入。
  5. capacity 不足时 root 内没有 shard 或输出文件。
  6. output 只能 take 一次;未 take 的 release 关闭,take 后 release 不关闭。
  7. output 只在 durable launching 后交给 ExecutorID 漂移不 spawn。
  8. tagged reject 幂等 closeambiguous throw 不由 Processor close。
  9. package 数、数据库、schema、依赖和 timer 均不增加。