Files
qinglong/docs/adr/ADR-0120-s3-compatible-immutable-remote-worker-artifact-store.md
T

99 lines
6.8 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-0120S3-compatible 不可变 Remote Worker Artifact Store
- 状态:Accepted(具体共享 store adapter 已实现,Worker execution composition 已由 ADR-0121 实现;range read、retention 与完整 Worker 产品生命周期仍默认关闭)
- 日期:2026-07-23
- 关联 RFCQL-RFC-0001 D-14、D-17、D-85、D-114、D-115、D-116、D-119
- 关联 ADRADR-0026、ADR-0027、ADR-0087、ADR-0115、ADR-0116、ADR-0117
## 背景
ADR-0117 定义了跨副本可见、digest-authenticated、put-if-absent 的 Artifact store port,但没有
具体生产 adapter。直接对最终 key 做流式 `PutObject` 有一个难点:SHA-256 只有完整消费 body 后才
确定,不能在发送 header 前作为 immutable metadata 写入;若目标已经存在,服务端又可能在完整读取
body 前返回 precondition failure,调用方无法证明 replay 内容与既有对象一致。
controller 本地临时文件虽然能先算摘要再上传,却会让每个并发请求消耗最多 64 MiB Pod ephemeral
storage,并重新引入“本地盘被误当成集群 authority”的风险。全量内存缓冲则直接破坏固定内存目标。
## 决策
### 1. 留在 cluster-control 的独立懒加载子入口
adapter 位于既有 `@qinglong/cluster-control/s3-artifact-store`,实现 ADR-0117 的
`ClusterRemoteWorkerArtifactStore`,依赖精确版本 `@aws-sdk/client-s3@3.1093.0`。不新增 workspace
package、schema、数据库连接、timer、queue 或 socket ownershipS3 client 由外层 composition root
创建并持有。Edge/Standalone 闭包不得导入该 cluster-only subpath。
### 2. 临时流式上传后做不可覆盖的 server-side promotion
首次写入顺序固定为:
1. `HEAD` 最终 opaque key;若已存在,完整消费并以固定内存计算 incoming SHA-256,只有
identity/length/digest/truncation 全等才返回 `already_stored`
2. 将 body 一次流式 `PutObject` 到 CSPRNG 临时 key,声明 exact Content-Length、SHA-256 checksum、
`If-None-Match: *` 和服务端加密;
3. `HEAD + ChecksumMode=ENABLED` 复验临时对象的 length、media type、owner nonce 与 full-object
SHA-256
4. 用同 bucket server-side `CopyObject` 提升到最终 key,设置 `If-None-Match: *`、替换为 canonical
metadata,并继续要求 SHA-256 checksum 和服务端加密;
5. 再次 `HEAD` 最终对象。只有 S3 checksum、metadata digest、length、identity hash 和 truncation
全部一致才返回 receipt;
6. 删除已证明由本次请求拥有的临时对象。删除失败只进入低敏 diagnostic,不能逆转已耐久 promotion。
Copy 的 409/412 竞争和 Put/Copy 成功但响应丢失都不能直接解释为失败或成功,只能通过最终
`inspect` 裁决。明确收到 Copy 成功才返回 `stored`;竞争或丢响应经证据恢复返回保守的
`already_stored`。永久对象从不被该 adapter 覆盖或删除。
### 3. key 与 metadata 不暴露业务 identity
最终 key 是 domain-separated SHA-256 identity digest 的两级 shard,不包含 Project、Run、Attempt 或
log Artifact ID。metadata 保存各 identity 的独立 domain-separated digest、byte length、content
SHA-256 和 `omitted|true|false` truncation,不保存原始业务 ID、Worker/Session、Lease token 或 callback
capability。`inspect` 从调用方 authority 重新派生并逐项比较。
临时 key 使用 UUID,另生成独立 256-bit owner nonce digest。若临时 ID 极端碰撞,未证明 owner nonce
的请求不得删除已有临时对象。部署必须对 `temporary/` 配置有界 lifecycle 作为崩溃垃圾回收,并让
runtime credential 只能删除该前缀;永久 `objects/` 的 delete/overwrite 权限不授予运行时。
### 4. 加密与 confused-deputy fence 必须显式
构造必须显式选择 SSE-S3 (`AES256`) 或 SSE-KMS,并在 KMS 模式给出 key ID;不提供明文 fallback。
AWS 部署可同时给出 12 位 `expectedBucketOwner`,它会传给 destination 与 Copy source 请求。若
S3-compatible 服务没有启用 KMS/SSE,启动配置或第一次写入必须失败,不能静默降级未加密对象。
### 5. 生产启用仍由更外层门禁控制
本 ADR 交付具体共享 store,但不自动注册 Worker Artifact/completion routes,也不自动启动 Worker
headless lifecycle。ADR-0121 已在 Worker 侧完成默认关闭的 production execution-plane composition
最终 Cluster Worker ingress 与完整 Worker 产品部署仍必须同时持有独立 mTLS/`ql3w` listener、最小权限
PostgreSQL service、该 adapter、Secret provider、Session/credential lifecycle 与 shutdown ownership
缺任一项继续 fail closed。
range read、retention、临时对象 lifecycle 的部署验证和本地 spool 删除仍是后续 Gate。
## 被否决的替代方案
1. **controller 本地临时文件**:并发磁盘成本随 64 MiB body 放大,并容易被误认为共享 authority。
2. **全量内存缓冲**:使 controller RSS 与 Artifact 大小和并发数相乘。
3. **直接最终 Put 后再补 metadata/tag**:内容与 receipt 不是一个不可变 publication,崩溃可见半状态。
4. **只信任 ETag**:单段、multipart、加密和不同 S3 实现的 ETag 语义不等于 canonical SHA-256。
5. **冲突后不消费 replay body**:无法证明同 identity 的内容一致,也会破坏 ingress complete-consumption gate。
6. **删除任何同名临时 key**:CSPRNG 碰撞或错误注入时会删除其他请求 authority。
7. **未配置 SSE 时明文继续**:把安全属性变成部署偶然行为。
## 验收证据
1. 11 个 adapter 单元测试覆盖首次写入、exact replay、不同内容、Copy race、Put/Copy 丢响应、
metadata/checksum 漂移、短读/超读、零字节、SSE-KMS、expected owner、cleanup diagnostic、临时碰撞
no-delete 与 pre-abort 零请求。
2. 真实 `minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e`
`RELEASE.2025-09-07T16-13-09Z`)启用临时 static KMS 后,通过首次写入、HEAD checksum、条件 Copy、
exact replay、不同内容拒绝和最终单对象集成验证。
3. 未配置 MinIO KMS 时 SSE-S3 明确返回 501adapter fail closed;测试没有加入明文兼容开关。
4. 本切片完成时 workspace importer 保持 23、TypeScript source file 为 293ADR-0121 后当前为 294
cluster/edge dependency audit 继续 `findings=[]`
5. 默认 Cluster Control 与 `/production` 子入口的独立进程加载证据均不含 `@aws-sdk/*`,只有
`/s3-artifact-store` 显式子入口加载 SDK;本切片完成时六种本机制品再次通过真实
pack/offline install/import,最大为 2,457,770 bytes、409 files、72 loaded modules 与
12,517,376 bytes RSS delta,且均不含 SDKADR-0121 后复跑的当前最大 RSS delta 为
12,566,528 bytes。