前面的流程已经很长了。
一份内容从 Generation 开始,要经过机器校验、独立审核、修订、复审、最终确认、内容包封存,然后还要进入整合站点层(Surface)审核。
如果这些全部通过,很容易产生一个直觉:
既然内容和整合站点都已经审核通过,那接下来直接激活不就行了吗?
我最开始也比较容易这样理解。
但真正实现 Activation 以后发现,这里面还差一个非常重要的问题:
审核通过时看到的状态,和真正准备写入激活状态时看到的状态,还是同一个状态吗?
如果不能证明这一点,那么即使之前所有审核都正确,最后一次写入仍然可能覆盖掉别人在这段时间里已经完成的变化。
所以 v2 最终没有把 Activation 设计成:
“Surface PASS 以后执行一次写入。”
而是拆成了:
先做只读 preflight,冻结当前前驱状态;真正 apply 时再验证这个前驱仍然没有变化。
这里就出现了三个很关键的概念:
activation-preflightprior_sha256- CAS
这篇就专门整理它们为什么存在。
Surface PASS 证明的是“审核通过”,不是“状态永远不会再变”
先把两个阶段分开。
整合站点审核主要回答:
当前准备整合进站点的内容,在完整站点上下文里是否正确?
它会关注:
- 页面之间的链接;
- 本地内容覆盖;
- structured content;
- glossary;
- carried 内容与 fresh 内容;
- 整合后的语义;
- 页面之间是否出现冲突。
如果通过,就可以证明:
这一组输入在审核时组成的整合站点状态是可以接受的。
但是它不能证明:
从审核结束以后,仓库里的激活权威状态永远不会再变化。
例如:
10:00:Package A 的 Surface PASS
10:03:另一个合法流程激活了 Package B
10:05:Package A 开始执行 Activation这时候 Package A 在 10:00 审核时看到的前驱状态,和 10:05 真正准备写入时的前驱状态,已经可能不是同一个了。
如果 Activation 仍然直接按照 10:00 的假设写入,就有可能把 10:03 的合法变化覆盖掉。
所以:
Surface PASS 是语言和整合质量证明,不是并发写入锁。
Activation 真正修改的是什么
这里还有一个容易混淆的地方。
Activation 并不等于:
把网站直接部署到 Production。
它更接近:
把某个已经完成正式生命周期的内容包,写入当前激活权威状态。
也就是说,工作流中仍然有:
Package Closure
→ Integrated Surface Review
→ Activation
→ Preview
→ ProductionActivation 解决的是:
哪一组经过证明的内容现在正式属于 activated state。
而 Preview 和 Production 还要继续验证真正的渲染、路由、浏览器行为和生产发布。
所以:
Activation 不是部署终点,而是正式内容状态发生变化的那个写入点。
也正因为它真的会改变权威状态,所以这里必须比普通只读检查更谨慎。
为什么不能直接执行 activation-apply
假设当前激活状态是:
当前状态:S0Package A 已经完成:
Generation
→ validation
→ Independent QC
→ A-only finalization
→ package closure
→ Integrated Surface Review PASS现在希望把 A 激活进去。
最简单的设计可能是:
读取当前状态 S0
→ 加入 Package A
→ 写成 S1问题在于:
“读取 S0”和“写入 S1”不是同一个瞬间。
如果中间发生了另一个合法写入:
读取时:S0
另一个流程写入:S0 → SB
当前流程仍然写入:S0 → SA最终可能变成:
SB 被 SA 覆盖而 Package A 本身可能完全没有问题。
Surface 也可能完全没有问题。
真正的问题只是:
写入时使用了已经过期的前驱假设。
所以 Activation 不能只问:
“我准备写什么?”
还必须问:
“我准备基于哪个当前状态写?”
这就是 prior_sha256 的作用
v2 的 Activation 会先执行:
activation-preflight
它是只读的。
输入不是 Surface receipt,也不是 Surface scope,更不是 Reviewer Bundle。
它的正式输入是:
package closure
preflight 会从这个 closure 出发,自动找到当前应该绑定的 Integrated Surface 状态,并重新计算当前 Activation plan。
其中一个非常重要的输出就是:
prior_sha256
可以把它理解成:
“我刚刚检查过的当前前驱状态”的精确 SHA-256。
例如概念上:
package closure:Package A
当前激活状态:S0
prior_sha256:hash(S0)
计划结果状态:S1
result_sha256:hash(S1)这时候 preflight 只是告诉维护者:
如果当前状态仍然是这个
prior_sha256,那么应用这次 closure 后,预期结果应该是这个result_sha256。
它还没有修改任何正式状态。
preflight 为什么必须是只读的
这点非常重要。
如果 preflight 本身就修改状态,那么:
“检查当前状态”和“正式写入状态”
就又混在了一起。
失败以后也会很难回答:
到底有没有改过?
所以 v2 明确要求:
activation-preflight不执行正式状态变更。
它负责的是:
- 读取 package closure;
- 找到当前 Integrated Surface;
- 检查 closure 与 Surface 是否仍然有效;
- 读取当前激活 authority;
- 计算 exact prior state;
- 计算预期 result state;
- 判断是否已经处于目标状态。
最后输出一个完整计划。
这一步失败时,应该可以明确说:
MUTATION=none也就是说:
检查失败不应该留下半个 Activation。
result_sha256 又解决什么问题
prior_sha256 描述的是:
写入之前应该看到什么。
result_sha256 描述的是:
如果这次 Activation 正确完成以后,应该得到什么。
于是一次 Activation 不再只是:
“运行了一个命令。”
而可以表达成:
已验证前驱:prior_sha256
已计划结果:result_sha256这使 Activation 从:
一个模糊的“切换动作”
变成:
从一个精确状态到另一个精确状态的转换。
这样后续检查就可以回答:
当前 state 到底是不是这次计划产生的结果?
而不是只看:
“某个 activation 文件是不是存在。”
真正的保护发生在 activation-apply
preflight 完成以后,维护者会拿到 exact:
prior_sha256
真正执行 activation-apply 时,不是只传 closure。
还必须同时提供:
刚才 preflight 得到的 exact
prior_sha256。
概念上就是:
输入 closure:Package A
预期前驱:prior_sha256=S0然后 apply 不会盲信旧 preflight。
它会重新读取当前状态,再做一次检查。
如果现在仍然是:
current sha256 == prior_sha256才允许继续写入。
如果不是,就停止。
CAS 到底是什么
这里使用的思路,本质上就是 CAS。
CAS 一般叫:
Compare-And-Swap
可以简单理解成:
只有当前值仍然等于我之前看到的值,才允许把它替换成新值。
放到 Activation 里就是:
如果 current == prior
→ 写入 result
如果 current != prior
→ 停止,不写它和数据库里的乐观锁非常像。
假设你打开了一篇文章准备修改。
你读取时版本号是:
version = 20你编辑了十分钟。
这期间别人已经保存了一次,版本变成:
version = 21如果你仍然直接保存,就可能覆盖别人刚才的修改。
更安全的方式是:
只有 version 仍然等于 20
→ 才允许保存
如果已经变成 21
→ 拒绝旧写入,重新读取Activation 的 prior_sha256 就承担了类似角色。
只不过这里比较的不是一个简单整数,而是:
当前权威状态的精确 identity。
为什么 SHA 比“状态名称”更可靠
假设只判断:
当前 package 数量 = 3这远远不够。
两个完全不同的状态可能都有 3 个 package。
甚至 package 名字一样,里面绑定的 closure、Surface、identity 也可能不同。
所以 Activation 不应该只比较:
- 数量;
- 文件名;
- 某个 loose status;
- “看起来已经差不多”的 scope。
而应该绑定:
完整 canonical state 的确定性 SHA-256。
于是只要真正参与 authority 的内容发生变化,prior_sha256 就应该变化。
这样才能准确检测:
从 preflight 到 apply 之间有没有出现并发状态变化。
一个最典型的并发场景
假设当前 activated state 是:
S0Package A 和 Package B 都已经完成各自的正式流程。
维护者先给 A 做 preflight:
A preflight:
prior = S0
result = SA接着又给 B 做 preflight:
B preflight:
prior = S0
result = SB如果先 apply A:
current S0 == A prior S0
→ A apply PASS
→ current 变成 SA接下来再执行旧的 B apply。
如果没有 CAS,B 可能仍然按:
S0 → SB写入。
这就可能把 A 刚刚激活的状态覆盖掉。
但有 CAS 以后:
当前:SA
B 旧 prior:S0
SA != S0于是 B 必须停止。
正确处理方式是:
读取当前 authority
→ 对 B 重新执行 preflight
→ 得到新的 prior=SA
→ 重新计算新的 result
→ 再 apply这样 B 就会基于 A 已经存在的最新状态继续前进。
prior mismatch 不是“再运行一次”就行
这一点很容易踩坑。
如果 activation-apply 报:
prior mismatch
它真正表达的是:
你之前检查过的那个世界已经不存在了。
这时候最危险的反应是:
“可能只是偶发问题,我再执行一次原命令。”
因为旧命令仍然携带旧 prior_sha256。
正确做法必须是:
停止
→ 读取当前 authority
→ 重新 activation-preflight
→ 获取新的 exact prior_sha256
→ 再决定是否 apply不能:
修改参数绕过 CAS。
也不能:
使用旧 prior 强行 replay。
因为 CAS 失败本身就是在保护刚刚发生的合法变化。
为什么 apply 还要重新 preflight
既然已经有 CAS,可能会问:
apply 只比较
prior_sha256不就够了吗?
v2 仍然会在 apply 时重新执行 preflight 逻辑。
原因是:
Activation 依赖的不只是一个 prior 文件。
从第一次 preflight 到 apply 之间,还可能有其他 currentness 发生变化,例如:
- package closure 不再 current;
- Surface binding 变化;
- authority 顺序变化;
- dependency 变化;
- 当前 package 状态变化。
所以 apply 不能只做:
SHA 一样 → 无条件写而要重新确认:
如果现在重新做一次 preflight,这个计划是否仍然成立。
然后再做 prior/current CAS 和并发变更检查。
因此:
preflight 的结果不是一张永久有效的写入许可证。
它只是当前时刻的精确计划。
already_active 为什么不是失败
Activation 还需要处理另一个现实问题:
命令可能被重复执行。
例如第一次 apply 已经成功。
但终端连接中断了,维护者没有看到最后的 PASS。
后来重新执行 preflight,发现:
当前 active state 已经就是预期结果。
这时候不应该再次写一遍。
也不应该报:
状态冲突。
正式结果应该能表达:
already_active=true也就是:
目标状态已经存在,不需要再次 mutation。
这是幂等性。
幂等和“重复执行没有关系”不是一回事
有时会把幂等理解成:
同一个命令随便执行多少次都行。
其实更准确的含义是:
如果目标状态已经正确存在,再执行不会制造第二次不同的状态变化。
例如:
第一次:
S0 → S1
第二次:
current 已经是 S1
→ already_active=true
→ 不重复写入但如果 current 已经变成:
S2那就不能说:
反正这个命令幂等,再跑一次。
因为这已经不是同一个前驱状态了。
所以:
幂等不能替代 currentness。
Activation 顺序也不能靠 package 名字猜
当多个 package 都准备 Activation 时,还会遇到一个问题:
哪一个应该先激活?
最简单的人工办法可能是:
“Tour 比 Learn/Docs 老,所以先 Tour。”
或者:
“文件名排序。”
这些都不可靠。
正式顺序应该来自:
当前 sparse authority / package workflow authority。
也就是说:
当前系统明确声明哪个 package 是下一步合法 Activation 对象。
这件事尤其重要,因为在 transition state 中,某些旧 coverage 命令可能因为当前状态尚未完全切换而变成 stale-sensitive。
所以不能为了找顺序,使用一个本身已经依赖“全部 activation 完成”的 coverage 结果反向推断。
原则应该是:
Activation order 来自当前 authority,不来自人的直觉。
为什么 transition state 特别危险
假设一个 locale 正处于:
Package A:active
Package B:已完成 closure,待 activation
Package C:还未完成这时候它既不是:
完整旧状态。
也不是:
完整新状态。
很多只适合稳定状态的检查,在这里可能得到:
stale如果错误使用这种 stale-sensitive coverage 来决定:
下一步谁应该 activation
就可能形成循环:
coverage 等 activation
activation 又拿 coverage 判断顺序所以 v2 把顺序 authority 单独明确下来。
这也是我后来越来越重视的一件事:
不同阶段的问题,应该读取属于那个阶段的 authority。
不要拿一个“最终完成状态”的检查器去推断中间过渡状态。
Surface receipt 为什么不能直接拿来做 activation 输入
另一个容易发生的错误是:
Surface 已经 PASS 了,那
activation-preflight --input应该直接接 Surface receipt 吧?
实际不是。
正式输入是:
package closure。
原因是 package closure 才是这个内容包进入 Activation 的正式生命周期输入。
Integrated Surface Review 是它需要满足的后续 gate。
所以 Activation 会:
从 package closure 出发
→ 自动绑定当前合法 Integrated Surface
→ 再计算 activation plan而不是让维护者手工把:
- closure;
- Surface receipt;
- scope;
- Reviewer Bundle;
拼在一起。
这样可以减少:
拿错 receipt、拿旧 scope、手工绑定错 Surface
之类的问题。
Surface PASS 以后为什么仍然不能马上 Production
这也是 Activation 设计带来的另一个边界。
完整路径是:
package closure
→ Integrated Surface Review PASS
→ Activation
→ Preview
→ ProductionSurface 主要证明:
组合后的内容语义和链接关系可以接受。
Activation 证明:
当前正式 activated authority 已经安全切换到新的内容状态。
Preview 再证明:
这份 activated state 真正通过 renderer、route、browser 和机器验收以后,运行结果也是正确的。
最后才进入 Production。
所以:
Surface PASS ≠ activated。
同样:
Activation PASS ≠ Production ready。
每个 gate 证明的是不同事情。
mutation=unknown 为什么必须立即停止
Activation 是正式状态变更。
所以最危险的不是:
明确失败。
而是:
不知道到底有没有写成功。
例如:
进程中断
磁盘错误
异常返回
写入结果无法确认这时候如果系统只能确定:
mutation=unknown就不能直接重新执行。
因为真实状态可能是:
情况 A:实际上完全没写
情况 B:已经写成功,但结果没返回
情况 C:写了一部分,状态异常如果不知道属于哪一种,再盲目 replay 就可能造成二次破坏。
所以这里的原则是:
mutation=unknown 时停止,并先 diagnosis。
只有重新证明 current authority 以后,才能决定恢复方式。
明确失败和未知失败必须区别对待
例如 preflight 失败:
preflight FAILED
MUTATION=none这是相对简单的。
因为可以明确知道:
正式状态没有变化。
修复问题后重新 preflight 即可。
但 apply 阶段如果发生:
mutation=unknown处理策略完全不同。
这时候首先需要回答:
当前 authority 到底是什么?
而不是:
命令为什么退出非零?
在正式状态机里,后者只是技术错误。
前者才决定是否安全恢复。
这就是 fail-closed 在 Activation 里的真正含义
项目里经常使用:
fail-closed
以前容易把它理解成:
出错就退出。
实际上,在 Activation 这种 mutation 阶段,它更具体:
只要无法证明继续写入安全,就不写。
包括:
- prior 不匹配;
- currentness 不足;
- Surface binding 不确定;
- authority 顺序不确定;
- concurrent mutation;
- mutation state 不可确认。
都必须停止。
目的并不是让工作流显得更严格。
而是避免:
一个本来完全合法的旧操作,因为执行得太晚,破坏了更新的合法状态。
CAS 保护的不是“当前任务”,而是别人刚刚完成的工作
这一点我觉得特别值得写出来。
看到 CAS 失败时,第一感受很容易是:
它阻止了我。
但换一个角度:
它可能刚刚保护了另一个已经成功完成的 Activation。
例如:
我的 prior:S0
当前状态:S1CAS 不知道 S1 是谁写的。
它只知道:
当前世界已经不是我当初检查的那个世界。
于是拒绝使用旧假设覆盖它。
这其实就是一个很重要的工程原则:
不确定新变化是否合法时,也不能因为自己的任务曾经合法,就覆盖更新状态。
为什么不能用“最后写入者获胜”
还有一种很简单的并发策略:
谁最后写,谁生效。
很多普通配置系统确实会这么做。
但对于翻译 lifecycle 来说并不合适。
因为这里每一个 activated state 背后都绑定着:
- Generation;
- machine validation;
- Independent QC;
- A-only finalization;
- package closure;
- Integrated Surface Review。
如果最后写入者可以无条件覆盖前一个合法状态,就等于:
后执行的旧计划可以抹掉先执行的新计划。
这会破坏整个证据链。
所以 Activation 更适合:
optimistic concurrency control
也就是:
大部分时间不加长期锁,但真正提交时必须确认前驱没有变化。
为什么不用一个很长时间的全局锁
既然怕并发,也可以想到:
preflight 开始就拿锁,直到 apply 结束。
理论上可以。
但实际工作流里 preflight 和 apply 之间可能包含:
- 人工检查;
- 终端操作;
- handoff;
- 多个 package 顺序处理。
长时间持有全局锁会带来:
- 阻塞;
- 死锁恢复;
- 中断清理;
- 锁状态不确定;
- 更复杂的运维。
CAS 的优势是:
不需要假设整个检查期间世界静止。
允许别人正常推进。
只是在自己真正提交时确认:
“我之前看到的前驱还在不在?”
如果不在,就重新计算。
这更符合当前这套增量工作流。
Activation 其实是一个很小的事务
后来我越来越觉得,可以把 Activation 看成一个小型事务。
它的逻辑大概是:
第一步:读取并验证 closure
第二步:绑定 current Surface
第三步:读取 current activation authority
第四步:计算 prior_sha256
第五步:计算 result_sha256
第六步:preflight 返回,只读结束
真正 apply 时:
重新执行当前检查
验证 current == prior_sha256
确认没有 concurrent mutation
原子提交新的 activation state
验证结果这里最重要的不是命令有多少。
而是保证:
从被证明的旧状态,只能转换到被证明的新状态。
我现在更认可的 Activation 原则
做到这里以后,我觉得 Activation 可以浓缩成几条比较清楚的原则。
第一:
Surface PASS 证明的是整合质量,不是未来写入时的并发安全。
第二:
activation-preflight必须是只读的,失败时明确MUTATION=none。
第三:
Activation 的正式输入是 package closure,由系统自动绑定 current Integrated Surface。
第四:
preflight 必须返回 exact
prior_sha256和result_sha256,让状态转换有明确的前驱和结果 identity。
第五:
activation-apply必须使用同一 closure 和 fresh preflight 得到的 exactprior_sha256。
第六:
apply 时必须重新验证 currentness,不能把旧 preflight 当成永久许可证。
第七:
prior mismatch 或 concurrent mutation 不是重试旧命令,而是重新读取 authority、重新 preflight。
第八:
already_active=true属于正常幂等结果,不应该再次 mutation。
第九:
Activation 顺序来自 current authority,不能根据 package 名称、历史经验或 stale-sensitive coverage 猜。
第十:
mutation=unknown必须停止诊断,不能盲目 replay。
最终我觉得最值得记住的一句话是:
一次操作曾经合法,不代表它现在仍然可以安全写入。
prior_sha256 和 CAS 真正保护的,就是:
从“我刚才看过这个状态”到“我现在真的要修改这个状态”之间,世界有没有发生变化。
Go 学习与文档多语言本地化项目
本系列完整记录 Go 学习与文档多语言本地化项目从 A Tour of Go 起步,逐步扩展到站点首页、项目说明页、学习内容、文档以及未来内容包的实际开发过程,包括架构设计、统一术语治理、整页翻译、结构保护、自动校验、独立质量审核、生产发布与长期维护。
项目源码:
✅ GitHub:shuijingwan/go-tour-i18n
语言版本:
当前跨项目目标语言池共包含 66 个语言版本。Go 项目本身目前提供 65 个社区 Production 语言版本,下面每一门 Go 语言版本都直接链接到其当前公开站点。语言列表沿用项目首页现有的展示顺序。
英语(美国)仅作为跨项目目标语言纳入列表,并不是单独的 Go Production 语言版本。当前 Go 的英文源内容仍为官方 generic English,因此英语(美国)链接到 Go 官方站点,而不是不存在的本地社区部署。
- 阿姆哈拉语
- 阿拉伯语
- 孟加拉语
- 葡萄牙语(巴西)
- 保加利亚语
- 加泰罗尼亚语
- 克罗地亚语
- 捷克语
- 丹麦语
- 荷兰语
- 英语(美国)(Go 官方英文站点)
- 英语(澳大利亚)
- 英语(加拿大)
- 英语(印度)
- 英语(新加坡)
- 英语(南非)
- 英语(英国)
- 爱沙尼亚语
- 菲律宾语
- 芬兰语
- 法语(法国)
- 法语(加拿大)
- 德语(德国)
- 德语(奥地利)
- 德语(瑞士)
- 希腊语
- 古吉拉特语
- 希伯来语
- 印地语
- 匈牙利语
- 印度尼西亚语
- 意大利语
- 日语
- 卡纳达语
- 哈萨克语
- 韩语
- 西班牙语(拉丁美洲)
- 拉脱维亚语
- 立陶宛语
- 马来语
- 马拉雅拉姆语
- 马拉地语
- 书面挪威语(Bokmål)
- 波斯语
- 波兰语
- 葡萄牙语(葡萄牙)
- 旁遮普语(果鲁穆奇文,印度)
- 罗马尼亚语
- 俄语
- 塞尔维亚语(西里尔文)
- 简体中文(中国大陆)
- 简体中文(新加坡)
- 斯洛伐克语
- 斯洛文尼亚语
- 西班牙语(西班牙)
- 斯瓦希里语(坦桑尼亚)
- 瑞典语
- 泰米尔语
- 泰卢固语
- 泰语
- 繁体中文(台湾)
- 繁体中文(香港)
- 土耳其语
- 乌克兰语
- 乌尔都语
- 越南语
项目正在持续扩展新的 Go 学习与文档内容,并长期维护各语言版本的术语一致性、翻译质量、独立审核、生产发布与后续更新。
本项目属于非官方社区多语言本地化项目,与 Go 官方无隶属、授权或背书关系。

