关于 troubleshooting-motoko-migrations
面向排查 mops 管理的 Motoko 迁移链问题的参考指南,解释运行时行为、常见诊断、冻结文件、已转换项目及迁移模式不可回退的原因。
主要功能
- 01说明迁移链的运行时行为
- 02将诊断信息对应到修复操作
- 03讲解冻结或重命名的迁移文件
- 04描述从旧模式转换的项目
- 05解释迁移模式的单向限制
适用场景
- 01排查 mops 兼容性诊断
- 02处理迁移文件写入失败
- 03检查初始状态非空的继承迁移链
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill troubleshooting-motoko-migrations安装后在 Claude Code 中输入/troubleshooting-motoko-migrations确认已加载。
详细介绍
Motoko 迁移故障排查
mops 管理的迁移链(migrations/)的故障模式参考。
常规工作时不要加载本文。 编写迁移、决定使用隐式还是显式迁移,或为新文件命名,都由 migrating-motoko-actors 负责——该技能的规则足以应对常规情况。遇到以下情况时再查看本文:
- 兼容性或迁移诊断结果与
main.mo中看到的内容不符 - 写入迁移文件失败,或你正考虑重命名/删除迁移文件
- 迁移链中的第一个文件有非空的
OldActor,而你不确定这是不是问题 - 任务要求移除迁移、恢复内联初始化器,或回退到
(with migration = ...) mops check报告的状态形状在当前源代码中并不存在
运行时如何决定要运行什么
- 整条迁移链都会编译进后端 wasm。不存在单独的迁移构件——更改任何迁移文件都会改变 wasm。
- 已应用的迁移按模块名(不带
.mo的文件名)记录在 canister 的持久化元数据中。 - 升级时,运行时只会重放模块名所对应的尚未应用的迁移。将落后多个版本的 canister 部署到新版本时,所有缺失步骤都会在一次升级中重放——无需通过中间版本部署来“追赶”进度。
- 全新安装时,迁移链中的每个文件都会按字典序重放。项目中曾添加过的所有迁移,都会在每次全新安装时运行,永久如此。
以下两个后果决定了这项技能中的大多数规则:
- 重命名迁移会改变其身份。 运行时会将重命名后的文件视为从未应用过,并在状态已不匹配时重放它;也可能跳过旧名称记录的工作。文件名是永久性的。
- 修改已应用的迁移对已运行过该迁移的 canister 不可见。 运行时会按名称跳过它,因此修改后的代码永远不会执行。在那里添加的稳定字段永远不会初始化,canister 访问该字段时会在运行时触发陷阱(
IC0503,“缺少迁移变量”类错误)。应通过新增迁移向前修复。
检查器会与什么进行比较
mops check 会根据已部署 canister 的稳定签名(其 .most 快照)验证迁移链,而不是根据本地 git 历史。因此:
- 诊断信息可能会提及源代码中已不复存在的字段——这些字段来自已部署的版本。
- 单个待处理迁移的限制只统计尚未部署的迁移。已上线的文件不计入限制。
.most标头中的稳定签名主版本号标识持久化模型:1表示经典模式,3表示旧版内联(with migration = ...),4表示增强型迁移链。项目达到4后就不能再回退(见EM 是单向的)。
你可能遇到的诊断信息
| 诊断信息 | 实际含义 | 操作 |
|---|---|---|
M0250 initialized stable field | 稳定 Actor 字段带有内联初始化器;该值必须由链(而不是 Actor 主体)提供 | 将其改为带类型的声明;把初始化表达式移入待处理迁移的 NewActor |
M0254 / M0267 initial actor requires field | main.mo 声明了一个没有迁移提供值的稳定字段。若已部署的签名也无法说明该字段,便会出现同样的问题 M0267 | 在待处理迁移的 NewActor 中添加该字段,并使用根据 OldActor 计算出的值或定义明确的默认值 |
M0170 compatibility error | 新 Actor 与已部署的签名不兼容 | 添加(或扩展)待处理迁移,明确说明转换方式 |
M0255 stable signature downgrade | 构建会将 canister 从增强型链回退到较旧的持久化模型——通常是因为链或 mops 迁移配置被删除 | 恢复已删除的内容;不支持降级 |
| Chain start does not match the baseline | 第一项迁移的 OldActor 未描述 canister 实际持有的状态——典型情况是转换后的项目,链从旧形态开始,而不是从 {} 开始 | 请参阅已转换的项目;不要将 OldActor 改写为 {} |
M0268 migration directory disagrees with deployed history | 已应用的迁移被编辑、删除或重新排序,导致迁移链与稳定基线记录的历史不再一致。此问题会在 mops build 时出现(该命令通过 --stable-baseline 合并已部署的基线);若 check-limit 将该迁移裁掉,则 mops check 可能不会报错 | 将已应用的迁移恢复为与部署时完全一致,并将后续更改写成新迁移。只能裁掉最早的迁移;不能删除中间已应用的迁移 |
M0278 file does not export a public migration function (warning M0251 on moc 1) | 链目录中的文件没有 public func migration,因此链会跳过该文件,其中的迁移也不会运行 | 将函数命名为 migration;将辅助模块移出链目录 |
M0014 effectful actor body (older toolchains) | 初始化工作位于 Actor 主体中 | 保持 Actor 主体为静态——请参阅静态 Actor 主体 |
IC0503 missing migration var (runtime, after deploy) | 稳定字段从未获得值——通常是因为已应用的迁移被编辑或重命名,或者添加字段时未添加迁移 | 添加一项新迁移来提供该字段;切勿通过编辑旧文件来修复 |
IC0505 invalid custom section (install only, build and check pass) | 链的稳定类型历史已超出平台自定义段的限制 | 无法在本地修复——停止添加迁移并报告此问题;请参阅为什么链必须保持简短 |
迁移文件写入失败不是工具问题——请参阅下一节。
冻结的迁移文件
在会冻结已部署迁移文件的平台上,每个迁移在成功部署后都会变为只读,写入工具会根据其模式位拒绝写入。
- 不要对其运行
chmod、删除或重命名,也不要绕过写入失败。冻结机制体现的是运行时不变量,而非策略偏好。 - 仍处于待处理状态(尚未应用)的迁移不会被冻结:请直接编辑该迁移,而不是为同一项更改添加第二个文件。
- 如果你需要的更改从概念上属于某个已冻结的迁移,请创建新的迁移,将当前状态转换为所需的结构。历史记录只允许追加。
已转换(旧版 → 增强版)项目
有些增强迁移项目并非一开始就采用这种方式创建,而是从旧版内联迁移持久化方式原地转换而来,并保留了实时数据。对于这类项目:
- 迁移链中的第一个文件通常是一个恒等迁移:其
OldActor是转换前的稳定结构,而NewActor会原样传递相同的数据。这是正确的做法。不要将其“简化”为OldActor = {},也不要重新填充这些字段——那会丢弃实时状态。 - 只有当迁移链确实从一个空 canister 开始时,
OldActor = {}才是正确的;也就是说,项目从创建之初就使用了这条迁移链。 - 转换前的历史记录仍然不可或缺:转换后项目的全新安装会从最后一个旧版架构开始重放,而不是从
{}开始。绝不要为了让迁移链从{}开始而试图删减、扁平化或“清理”它。 - 首次迁移中关于某个字段仅存在于转换前结构的诊断是预期行为,由平台的构建配置处理。不要为了消除这些诊断而重构迁移链。
项目也可能继承这样的迁移链,但自身并未经过转换——分叉或混编一个已转换的项目时,会原样复制迁移链,包括第一个迁移。无论是哪种情况,规则都相同,而且从迁移链本身就能直接看出:最早的文件中,非空的 OldActor 意味着状态早于迁移链存在,因此该文件当前的写法是正确的。
受保护的配置
如果托管平台拥有 mops 迁移配置——即 [canisters.<name>.migrations] 部分、其中的 chain 路径和 check-limit——你就不能添加、删除或修改它,工具也会拒绝此类尝试。
- 切勿为了消除“待处理迁移过多”错误而提高或降低
check-limit。 正确做法始终是将额外的更改合并到一个待处理迁移中。错误中列出的每个文件都尚未应用,因此可以编辑其中任意一个。 - 迁移链目录由
chain = ...指定。 通常是src/backend/migrations,但以非默认 canister 名称导入的项目可能使用不同路径。请查看 mops 配置,不要想当然地假设路径。 - 如果项目存在迁移链,但迁移部分似乎缺失,请勿自行重新添加;应报告此不一致。
EM 只能单向迁移
项目一旦使用迁移链,就没有受支持的方式可以回退到旧版持久化机制。以下机制会分别强制执行这一点:
- 每个项目的持久化模型一经确定便会固定下来,之后不会重新评估。
- 编译器会拒绝签名降级(
M0255),而部署到增强型 canister 上的旧版 wasm 会在运行时触发陷阱。签名版本只会向前演进。 - 已应用的迁移通过模块名称识别,因此无法通过编辑或重命名文件来改写历史记录;在会冻结已部署迁移的平台上,历史记录则完全无法改写。
- 转换后项目的全新安装会永久依赖转换前的历史记录。
因此,如果任务要求你删除 migrations/ 目录、在稳定字段上恢复内联初始化器,或重新引入 (with migration = ...),请明确说明该项目无法这样做,并提供只向前迁移的替代方案:创建一个新迁移,将状态调整为用户实际需要的结构。
为什么链必须保持简短
每个已部署版本只对应一个迁移的规则并非官僚主义。迁移链太长最终会变得无法部署,而且故障无法恢复:
- 编译后的 wasm 会携带一个自定义区段,其中编码了完整的稳定类型历史。这个区段会随着每次迁移以及稳定类型本身的大小而增长,直到超过平台对这类区段设定的 1 MiB 限制。稳定类型不太复杂时,通常要到数百个文件才会触及限制;类型越丰富,触及限制所需的文件就越少。
- 到了这一步,
mops build和mops check仍会成功——只有安装会被拒绝(IC0505,自定义区段无效)。检查通过并不代表链可以部署,重新部署或重试也无法恢复:超大区段已固化在构建产物中。 - 每个文件都会在每次全新安装时重放,永远如此,而且整个过程都在一条消息中执行。垃圾回收只会在消息边界运行,因此整个重放期间,每个中间状态都会一直存活——数据量大的迁移可能会在链长远低于区段限制时就耗尽内存。
- 全新安装会按步骤递归执行,因此调用栈深度与链长成正比。
- Wasm 大小和编译时间会随着每一步增长,而
.most签名会序列化整条链。
工具链不会限制链长,因此每个版本只对应一个迁移的规范是项目避免触及这条悬崖的唯一保障:将变更合并到待处理的迁移中,保持稳定类型精简,并且绝不要添加一个函数体只是 old 的迁移。
静态 actor 函数体
actor 函数体不得执行任何操作。稳定字段是类型声明;它们的值来自迁移链。
较早的工具链会直接拒绝任何带副作用的 actor 函数体;之后,编译器对临时绑定放宽了这项检查——但无论编译器是否报错,这条规则都依然适用,因为顶层初始化会在每次升级时重新运行,从而覆盖现有状态。
初始化只能放在以下两个位置之一:
- 迁移函数中,该函数会在安装或升级时为每个 canister 运行一次;或
- 在正常运行期间按需调用的幂等逻辑中(例如在 update 方法内检查工作是否已完成)。
无法解决时
不要通过放宽受保护的配置、删除或重命名历史记录,或重写已冻结文件来腾出继续操作的空间。这些做法会把构建失败变成数据丢失。如果迁移链与已部署签名确实存在上述情况无法解释的差异,请停止并报告不匹配:检查器指出了哪些字段、最新迁移的 NewActor 声明了什么,以及 main.mo 声明了什么。
相关技能
migrating-motoko-actors— 如何编写迁移(常规路径)writing-motoko— 语言参考、actor 和 mixin 规则、核心库