Skip to content

平台形态的迁移门禁:带自检的数据迁移 + 部署级标记 —— file-as-reference 回收与 strict 翻转都依赖它 #3617

Description

@os-zhuang

把 ADR-0104 D3 wave 2 收尾时暴露出来的同一类问题合并成一条。核心是一个设计前提错了,而它同时卡住了两件事:file-as-reference 的回收(#3459 PR-5b)和 D1/D2 的 strict 默认值翻转(#3438)。


一、错在哪

wave 2 给回收定的验收门是这样的:

在真实租户上跑回填 → verifyFileReferences 连续 ≥7 天零阻断项 → 才允许开启回收。

这是一份 SaaS 形态的运维 runbook,隐含前提是"我们运营那个租户、我们能观察它、我们来翻开关"。

但 ObjectStack 是开发平台。 第三方开发者在上面开发元数据应用,版本交付出去之后,每个部署自己决定何时升级;他们的数据我们看不到,也不该看到。根本不存在一个"我们的观察窗口"可以让所有人等。

二、正确的结论:证据要求不能取消,只能换地方

R4 的本质没变——一本账在被证明与现实一致之前,不能授权删除字节。改变的是产生证据的主体:从"我们"变成"每个部署自己"。

所以需要的不是一段一次性脚本(跑完可以无视输出,等于没有门禁),而是:

一个带自检门禁的数据迁移步骤——回收开关由「这个部署自己跑过并通过了」来打开,而不是由版本号打开。

形状

os migrate files-to-references
  1. 回填(默认 dry-run,--apply 才写)
  2. verifyFileReferences
  3. 仅当零阻断项 → 写入部署级标记
       { id: 'adr-0104-file-references', verified_at, blocking: 0 }
  4. reap guard / strict 开关读这个标记,而不是读全局 flag

由此得到的性质

  • 升级到新版本不会自动打开回收。 装了新版 ≠ 开始删字节。
  • 第三方开发者不需要理解 R4。跑 os migrate,绿了就完事;红了报告直接指出哪条记录持有一个没人拥有的文件。
  • 没跑 / 没通过 → 文件继续永生。 浪费存储,零数据丢失——"fail toward retention" 这条默认值在部署维度同样成立。
  • 叠加已有的 30 天墓碑窗口,每个部署有自己独立的 30 天回头路,不需要共享任何人的观察期。真正不可逆的时刻是各自开启后的第 30 天,不是升级那天。

三、⚠️ 同一个错误在 #3438 也埋着

D1/D2 的 strict 默认值翻转有一模一样的问题。

我们不能替所有第三方部署决定「从现在起拒绝遗留 inline blob」。一个部署如果还没回填完,strict 一开,任何改写 media 字段的 update 都会被拒——正常工作的应用会挂,而且是我们替他们做的决定

所以 #3438 也该走同一个机制:这个部署回填完、对账干净了,strict 才对它生效,而不是某个版本号一到就全体翻转。

两件事应当共用同一个「本部署已完成 file-as-reference 迁移」标记。 这也是把它们合并成一条 issue 的原因——分开做会做出两套不兼容的门禁。

四、现实的缺口(真正的工作量在这)

查过当前 main(d2a8695):

现状 结论
os migrate = plan.ts / apply.ts / meta.ts,围绕 ChangeSet 纯 DDL(加字段/改字段/建对象/执行 SQL)
packages/spec/src/system/migration.zod.ts 只有 schema 操作类型,没有「带校验门禁的数据迁移」概念
运行时迁移状态 不存在(sys_migration 只有 spec 层类型,没有运行时表)
backfillFileReferences / verifyFileReferences 已实现(#3535 / #3534),但只是库函数,没有命令行入口,运维跑不了

五、工作项与顺序

# 事项 规模 备注
1 os migrate files-to-references 命令(包装两个已有库函数,dry-run 只读) 建议先做:零风险,第三方下个版本就能跑,立刻拿到真实数据规模和外部 URL 清单
2 数据迁移 + 门禁基础设施(部署级标记的持久化与读取) 比 PR-5b 本身大 #3438 也要用
3 PR-5b 回收:改读标记而非全局 flag 两半必须同一 change,见 #3459
4 #3438 strict 翻转:改读同一个标记

之前 #3459 里把讨论集中在第 3 项,顺序是反的——真正卡住第三方的是第 1、2 项

六、有一类东西自动化不了,但不阻断

回填里的外部 URL(https://cdn…)要不要迁到显式的 url 字段,是建模决定,只能由应用作者做。好在它们天然不阻断:外部 URL 根本不是 sys_file,进不了 GC。所以报告里单列为 advisory 即可(PR-6 已经是这么设计的:external_url_skipped 是 advisory 不是 blocking),不卡住回收门禁。

相关

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions