5.4 KiB
ADR-0115:有界 Worker 文件 Log Artifact 与 Output Ownership
- 状态:Accepted
- 日期:2026-07-23
- 关联 RFC:QL-RFC-0001 D-24、D-26、D-71、D-110、D-112、D-114
- 关联 ADR:ADR-0024、ADR-0026、ADR-0071、ADR-0111、ADR-0113
背景
ADR-0113 只把 logArtifactId 送到了 Executor launch,没有具体 Worker writer,也没有定义
materializer dispose 与异步进程输出之间的所有权。若在 Processor.launch 的 finally 中
关闭 writer,Executor.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_NOFOLLOW 和 0600,
打开后复验 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:
- Secret 全部闭合后才 prepare;
- Processor 校验环境和日志 ID;
- inbox CAS 写入 durable
launchingbarrier; - 只调用一次
takeOutput(),复验 sink ID 与 materialized ID 完全相同; - 调用
Executor.start时 ownership 转移给 Executor; - materializer finally 仍释放 Secret 和 preparation,但 handed-off release 不关闭 writer。
Executor 对所有已知 terminal path 负责关闭 writer。tagged rejected 明确证明未 spawn,
Processor 再执行一次防御性幂等 close;抛错或非法结果属于 unknown spawn,Processor 禁止
关闭,交给 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 在这些闭环前不得删除。
被否决的替代方案
- writer 作为新 package:没有独立部署或供应链边界,只制造 package 碎片。
- Executor 自己选择日志路径/ID:控制面 Artifact identity 与实际字节分叉。
- 在 materializer dispose 中无条件 close:会截断已经交给异步 Executor 的输出。
- spawn barrier 前 handoff:barrier CAS 失败会产生无 owner writer。
- unknown spawn 时 Processor close:可能关闭正在运行进程的唯一日志出口。
- Edge/Node 两套协议或实现:只需显式策略不同,不应复制状态机。
- 后台 timer 做 quota/cleanup:增加低配设备常驻资源,且 timer 不能证明进程终态。
验收证据
- 相同 authority ID 稳定,新 offer ID 不同且不暴露输入。
- root/shard/file 权限分别为
0700/0700/0600,symlink target fail closed。 - reopen 使用同一 ID 并 append 已接受 prefix,不 truncate。
- 总 quota 只保存剩余 prefix并返回
quota_exceeded;oversized chunk 零写入。 - capacity 不足时 root 内没有 shard 或输出文件。
- output 只能 take 一次;未 take 的 release 关闭,take 后 release 不关闭。
- output 只在 durable
launching后交给 Executor;ID 漂移不 spawn。 - tagged reject 幂等 close,ambiguous throw 不由 Processor close。
- package 数、数据库、schema、依赖和 timer 均不增加。