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

95 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 中
关闭 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`
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 前 handoff**barrier 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/0600`symlink target fail closed。
3. reopen 使用同一 ID 并 append 已接受 prefix,不 truncate。
4. 总 quota 只保存剩余 prefix并返回 `quota_exceeded`oversized 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 均不增加。