关于 troubleshooting-motoko-migrations
面向使用 mops 管理 Motoko 迁移链的开发者,介绍故障诊断与安全修复方式,涵盖运行时行为、检查器诊断及只能向前演进的规则。
主要功能
- 01迁移运行时与重放规则
- 02解读 mops check 诊断
- 03已部署与待处理文件规则
- 04转换型旧项目的处理方式
- 05长迁移链的限制与风险
适用场景
- 01诊断与已部署稳定状态不符的问题
- 02处理已应用迁移文件的写入失败
- 03判断首个 OldActor 非空是否合理
- 04应对回退增强迁移的请求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --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 初始化了稳定字段 | 稳定 Actor 字段带有内联初始化器,该值必须由链提供,而非 Actor 主体 | 将其改为带类型的声明;把初始化表达式移到待处理迁移的 NewActor 中 |
M0254 / M0267 初始 Actor 需要字段 | main.mo 声明了一个没有迁移提供的稳定字段。若部署签名也无法说明该字段,则会出现 M0267 | 在待处理迁移的 NewActor 中添加该字段,并使用从 OldActor 计算得出的值或定义明确的默认值 |
M0170 兼容性错误 | 新 Actor 与已部署签名不具备稳定兼容性 | 添加或扩展待处理迁移,明确说明转换方式 |
M0255 稳定签名降级 | 构建会将 canister 从增强型链退回较旧的持久化模型,通常是因为移除了链或 mops 迁移配置 | 恢复已移除的内容;不支持降级 |
| 链的起点与基线不匹配 | 第一个迁移的 OldActor 未描述 canister 实际持有的状态——典型情况是转换后的项目从旧版结构而非 {} 开始 | 请参阅转换后的项目;不要将 OldActor 改写为 {} |
M0268 迁移目录与已部署历史记录不一致 | 某个已应用的迁移被编辑、删除或重新排序,因此链与稳定基线记录的历史不再匹配。此问题会在 mops build 时出现(它通过 --stable-baseline 纳入已部署的基线);若 check-limit 将该迁移排除在外,则 mops check 可能不会报错 | 将已应用的迁移恢复到与部署时完全一致的状态,并将后续更改写成新的迁移。只能裁剪最旧的迁移;不能删除中间已应用的迁移 |
M0014 有副作用的 Actor 主体(较旧工具链) | 初始化工作位于 Actor 主体中 | 保持 Actor 主体静态——请参阅静态 Actor 主体 |
IC0503 缺少迁移变量(部署后的运行时错误) | 稳定字段从未获得值——通常是因为已应用的迁移被编辑或重命名,或者添加字段时未提供迁移 | 添加新的迁移来提供该字段;绝不要通过编辑旧文件来修复 |
IC0505 自定义区段无效(仅安装时出现,构建和检查均通过) | 链的稳定类型历史超出了平台自定义区段的限制 | 无法通过本地操作修复——停止添加迁移并报告此问题;请参阅为什么链必须保持简短 |
迁移文件的写入失败并非工具问题——请参阅下一节。
冻结的迁移文件
在会冻结已部署迁移文件的平台上,每个迁移在成功部署后都会变为只读,写入工具会根据其模式位拒绝写入。
- 不要对其运行
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 规则、核心库