冷 Rollout 压缩是后台 best-effort 表示变换,不改变逻辑历史;任何需要追加或引用物理前缀的路径都会先把 .jsonl.zst 安全物化回普通 JSONL。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究冷压缩筛选、原子发布和追加前解压,不讨论 JSONL 内容语义。输入:达到冷却阈值且未被引用的 plain JSONL;状态所有者:后台 compression worker 与 materialize_rollout_for_reference;成功结果:可透明读取的 zstd 表示,或可继续追加的 plain 表示。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。
状态边界
| 问题 | 本节答案 |
|---|---|
| 输入 | 达到冷却阈值且未被引用的 plain JSONL |
| 状态所有者 | 后台 compression worker 与 materialize_rollout_for_reference |
| 成功产物 | 可透明读取的 zstd 表示,或可继续追加的 plain 表示 |
| 研究范围 | 研究冷压缩筛选、原子发布和追加前解压,不讨论 JSONL 内容语义 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 扫描 active/archived 冷文件。
- 排除引用和变化中的对象。
- 压缩到临时文件并回读验证。
- 发布 zst 后删除 plain。
- 追加时解压临时文件并恢复 plain。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
Worker 有时间和并发预算
默认只考虑至少 7 天未修改的文件;run marker 防止 6 小时内重复启动,单轮最长 5 小时,并发压缩 job 上限 2,zstd level 为 3。它不阻塞前台启动结果。
引用中的物理文件不能压缩切换
扫描会构建 RolloutReferenceIndex,跳过被 history_base 指向的源与 Fork pointer。引用读取依赖稳定 byte offset,不能在无协调时把表示换掉。
发布前后都重新检查
Worker 先压缩临时文件并验证能够解码,再检查源文件身份/mtime/size 没变化;发布后再次确认,最后才删除 plain,降低并发追加造成的数据丢失风险。
读取允许短暂表示竞态
打开 plain 或 .zst 时,若恰好撞到另一进程切换表示导致 NotFound,会以 50ms 间隔重试最多 3 次。读取接口对上层隐藏具体扩展名。
追加必须物化 plain
解压先写临时文件、sync、保留权限和 mtime,再以 no-clobber 方式发布 plain。确认 plain 存在以后才删除压缩文件,然后 Recorder 才执行尾部换行检查与追加。
Python 风格伪代码
下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:
async def compress_if_cold(path, refs):
snapshot = await stat_identity(path)
if snapshot.age_days < 7 or refs.contains(path):
return SKIP
temp = await zstd_encode_to_temp(path, level=3)
await verify_can_decode(temp)
if await stat_identity(path) != snapshot:
return RETRY_LATER
await publish_noclobber(temp, path + '.zst')
if await stat_identity(path) == snapshot:
await remove_plain(path)
async def materialize_for_append(zst_path):
temp = await decode_to_temp(zst_path)
await temp.sync()
plain = await publish_noclobber(temp, remove_suffix(zst_path, '.zst'))
if await exists(plain):
await remove(zst_path)
return plain
阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| 压缩期间源文件变化 | 临时 zst 基于旧快照 | 不能发布为当前历史 | 身份重检失败后放弃 |
| zst 验证失败 | 临时产物不可读 | plain 必须保留 | 删除 temp 并记录警告 |
| 读取撞上表示切换 | plain/zst 名字短暂变化 | 首次 open NotFound | 有限次数延迟重试 |
| plain 未发布先删 zst | 两种表示都不存在 | 历史丢失 | 确认 plain 后才移除 zst |
这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。
必须保持的不变量
- 任一时刻至少有一种完整可读表示
- 压缩不改变 JSONL 解码后的 RolloutLine 序列
- 被引用祖先不在后台无协调切换表示
- 追加只发生在 plain JSONL 上
这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。
设计取舍
透明压缩节省冷历史空间,却引入表示竞态、临时文件和引用感知;因此它被设计成可放弃的维护任务,而非写入成功条件。
源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。
Mini Codex 复刻
后台任务只压缩 immutable-by-check 的冷文件;读端尝试 plain/zst,写端强制 materialize,故障注入确保发布顺序不会出现双缺失。
复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。
源码导航
- codex-rs/rollout/src/compression.rs:冷文件策略、zstd 发布、读取重试与重新物化
- codex-rs/rollout/src/rollout_reference_index.rs:压缩扫描使用的引用保护信息
相邻测试也很重要:
- codex-rs/rollout/src/compression_tests.rs:压缩读取、追加物化、冷文件选择与引用跳过
本节结论
冷 Rollout 压缩是后台 best-effort 表示变换,不改变逻辑历史;任何需要追加或引用物理前缀的路径都会先把 .jsonl.zst 安全物化回普通 JSONL。
评论
登录后即可评论