Phorge 现代化改造实战(四):拆分模块到 Gorge,无侵入改造不等于不碰文件
内容提要
本文介绍Phorge现代化改造中拆分语法高亮为Go服务(gorge-render)的经验。核心在于通过配置驱动和组合方式无侵入接入,避免修改上游核心文件,控制fork冲突面。同时强调语义层面的兼容,如保留Future并发、异常降级协议和语言映射细节,并通过分层测试和契约固定架构,平衡服务独立与开发成本。
延伸解读
冲突面比改动行数更重要
文章强调,fork 上游时,债务不按改动行数计价,而按冲突面计价。新增独立文件通常不会与上游冲突,而直接修改活跃核心文件则可能在每次同步时产生手工合并。因此,增加功能时应优先寻找宿主已有的扩展点,如配置驱动的类实例化,尽量通过新增文件完成,以控制长期维护成本。
语义兼容是无侵入的关键
无侵入不仅指不修改上游文件,更指不破坏上游未写在接口签名中的语义。文章列举了三个例子:返回 Future 以保留并发调度、抛出特定异常以触发宿主降级协议、保留语言大小写映射以避免语义错误。这些隐含约定需要从调用方、错误处理和历史数据中理解,否则即使类型正确、测试通过,也可能悄悄架空关键机制。
测试应覆盖兼容边界而非仅功能
Gorge 的测试代码量超过生产代码,因为迁移成本主要在兼容边界。文章指出,测试不能只判断返回值是否非空,而要比较最终 lexer 的身份,因为存在“能运行但含义错误”的对象。同时,分层测试(如用 go/parser 检查 import)能将架构约束从文档变成可执行的检查,防止随手 import 破坏架构。
配置生命周期需区分用户偏好与部署拓扑
文章对比了 local.json 的持久化策略与 Gorge 配置的幂等更新策略:用户配置应持久化,而部署拓扑(如服务地址、Token)应跟随编排变化。两类配置即使写入同一文件,也应采用不同生命周期。此外,部署服务与切换流量被刻意拆成两步,以便先验证健康状态再切换,回滚也更简单。
Q&A
Phorge 现代化改造中,如何将语法高亮功能拆分为独立的 Go 服务?
通过新增一个继承 PhutilSyntaxHighlighterEngine 的类 PhabricatorGorgeSyntaxHighlighterEngine,并利用配置 syntax-highlighter.engine 切换引擎,将高亮请求转发给常驻的 Go 服务 gorge-render。该服务提供 POST /api/highlight/render 接口,接收源码和语言名,返回带 Pygments CSS 类名的 HTML。
为什么在 Phorge 中拆分模块时要避免修改上游核心文件?
因为直接修改上游核心文件(如 PhutilDefaultSyntaxHighlighterEngine)会导致每次同步上游时产生冲突,增加手工合并成本。通过配置驱动和组合方式,将主要逻辑放在新增文件中,可以控制冲突面,降低 fork 的维护成本。
Phorge 的高亮接口返回 Future 有什么意义?
返回 Future 允许调用方(如 Differential)使用 FutureIterator 并发处理多个高亮请求,例如同时渲染 diff 的左右两侧,减少等待时间。如果适配层提前 resolve,会丢失并发语义,导致性能下降。
在接入 Gorge 服务时,如何处理异常降级?
适配层不应自行捕获异常并返回默认结果,而应抛出 PhutilSyntaxHighlighterException,让 Phorge 原有的降级机制(如默认高亮和页面错误提示)生效。这样能保持可见降级,避免静默失败。
为什么语言映射表需要保留大小写敏感?
因为 Phorge 从文件名推导语言时未统一转小写,例如 foo.R 会以 'R' 到达高亮器。PHP 语言表中 'R' 映射为 'splus',而 'r' 映射为 'rebol',大小写不同语义。如果 Go 服务直接转小写,会导致错误映射。因此应先按原始字符串查表,未命中时才尝试小写。
Gorge 项目如何组织代码结构以平衡服务独立与开发成本?
Gorge 采用模块化单体结构,开发时所有服务共享一个 Go module 和 Dockerfile,通过 SERVICE 参数选择入口;运行时按需部署为独立服务。代码分为 internal/platform、internal/contracts、internal/render 三层,并通过分层测试确保 platform 不依赖业务域。
为什么 Gorge 的测试代码比生产代码多?
因为迁移中最昂贵的部分在于兼容边界,需要大量测试来确保与 Phorge 的语义兼容,包括语言映射、异常处理、Future 并发等。测试覆盖了契约、分层、端到端等多个层面,以固定架构和防止回归。
在部署 Gorge 服务时,为什么需要先启动服务再切换引擎?
部署服务(docker compose up)和切换引擎(bin/config set)是两步操作,这样可以先验证服务健康、Token 和语言列表,再让真实页面调用。回滚时只需切换引擎回默认类,无需停止服务。