前排提示,本文全文长 1.6w 字,由十个故事组成。出于微信公众号的展示特点,我将其切成若干个部分分开推送。如果想要单页读完全部内容,请点击阅读原文跳转网页,网页展示有目录,可以快速导航。
Protobuf
虽然我给 GCP SDK 的 pull request 被挂了几个月,直到现在也还没人搭理,但是参与 Google 的另一个开源项目 Protobuf[1] 的体验还是不错的。
我在瞎鼓捣 Pulsar Ruby Client 的时候,碰到了 Pulsar 的 Proto 文件定义的枚举类型内部字段是小写字母开头,而由于 Ruby 没有枚举类型,Protobuf 把枚举类型的字段映射成 Ruby 里的常量,Ruby 的常量又必须是大写字母开头,最终导致定义失败的问题。
虽然这个问题看起来前提条件很复杂,但实际上是一个 Ruby 开发者和 Proto 定义的消息交互时非常容易遇到的情形。上游在 2016 年就有相关报告:
•Make it possible to have lowercase enums in Ruby[2]
逻辑上的解法其实很简单,在定义枚举字段映射到 Ruby 的常量的时候,自动把字段名首字母大写就行了。这样既不会影响现有代码,又能够解决原来常量定义失败的故障。虽然对字段名做了自动调整,但是原本小写字母开头的常量定义是失败的,根本也用不了,而实际到二进制转换不看名字只看编号,到文本的转换走的是符号解析支持小写字母开头。
经过这轮分析以后,我确定这个路径是可以走通的。于是从报错信息定位到相关代码,把“自动大写字段名首字母”的逻辑原地打了个补丁上去。当时我也不懂怎么触发测试,也不知道会不会有其他问题,但是先做自己能做的事情,提交到上游让其他干系人发现有人在努力解决这个问题,并且已经有一些进度了,这能够在原本大家都观望的环境里抛出一个凝结核,吸引用户测试补丁和维护者评审代码。
•Auto capitalize enums name in Ruby[3]
不同于 GCP SDK 的源码只读状况,Protobuf 的维护者隔天就帮我触发了测试,这让我感觉到这个社群还是会关注我的工作的。一周以后,Ruby 模块的维护者之一 Jason Lunn 开始 review 我的代码,由此开始了近一个月的 review 循环。
中间过程我就不再赘述,如果你去看我提交的补丁的对话,你就会发现:
1.因为虽然我对这个改动有需求,但是不是特别着急,所以对话经常是以周为单位。每周末我闲着没事的时候,有时就能想起来还有这件事没搞完,于是看一下 review 意见和测试结果还有哪些要改的,集中思考和解决一波。2.因为我对 Ruby 并不熟悉,而且一上来搞的就是 Ruby + C 和 JRuby 的元编程,所以这个过程里我其实不是一开始就知道符号的部分不用动,写出了一堆问题。解决问题的方向错了,reviewer 好像也没看出来,大部分时间都是我自己在纠结、测试和补丁之上的补丁,碎碎念的状态活像一个孤独患者自我拉扯。3.因为 contribute code 最好还是本地可以跑全量测试提升反馈效率,所以整个过程下来我把 bazel 这套构建尤其用于 Ruby 项目编译的各种 trouble shooting 都搞了个遍。以前我总觉得 bazel 的概念晦涩难懂,但是实际直接用起来一个配置好的项目,不仅体验不错,还帮我理解了很多设计的原因。4.最后,虽然我在错误的道路上走了太远,甚至一度以为这件事情没法实现,不过就像我上面对问题的总结,我回归到一开始要解决的问题,加上一个月来对这段代码的深入理解,终于发现了正确的解法,最终用不到 50 行代码就把问题给解决了。
代码合并以后,我自然是再次用定型文催上游发布我好早点用上。不过这次上游没有给我反馈,于是我主动观察了一下发布的规律,发现 21.x 的版本每半个月到一个月就会发布新版本,然而由于我的补丁只在 master 分支上,只有等到 22.0 发布的时候才能用上。我觉得这个改动不大,所以就询问维护者能不能 backport 到 21.x 的分支上赶上下一个短周期的发布。
另一个维护者 Mike Kruskal 支持这个做法,并且跟我确认了 21.x 和 22.0 的发布节奏。我得到支持以后就把这个不到 50 行的补丁轻松地 backport 到了 21.x 版本,并在三天前得到合并。期待发布中。
这个故事可以拓展成一个典型的上游优先模式。许多程序员在面对自己的问题的时候,一开始做出的改动就跟我原地打一个 monkey patch 一样,对自己的用例有效,其他自己用不到的地方就不管了。但是把自己的修改提交到上游接受评审的时候,才发现原来这个改动可能牵扯到这个那个模块。上游同时被许多下游依赖着,因此它们所选择的解决方案很可能不是 monkey patch 的方式。通过这样的上游优先参与,能够逐渐锻炼自己下游使用修改时候符合上游的设计哲学,从而尽力避免由于理念不同而最终不得不分支的情况。
当然,这个例子可能稍显简单了。一个年代比较久远的例子是 2019 年前后我在 Flink 社群参与发起和实现的 FLIP-73[4]、FLIP-74[5] 和 FLIP-85[6] 这三个提案。我在腾讯内部其实做了不一样的实现,在上游社群和其他 committer 沟通以后形成了最终上游的解决方案。不过关键的思路是一样的,所以内部版本追上上游也不困难,不会因为有截然不同的假设导致被存量拖死。
另外,Protobuf 的问题是 2016 年提出的,今年我解了,参考某司解决一个 etcd 悬挂三年的边缘问题吹上天,我是不是可以标题党地写一个《震惊!他竟然解决了 Protobuf 一个长达六年的痛点!》。
References
[1]
Protobuf: https://github.com/protocolbuffers/protobuf[2]
Make it possible to have lowercase enums in Ruby: https://github.com/protocolbuffers/protobuf/issues/1965[3]
Auto capitalize enums name in Ruby: https://github.com/protocolbuffers/protobuf/pull/10454[4]
FLIP-73: https://cwiki.apache.org/confluence/display/FLINK/FLIP-73%3A+Introducing+Executors+for+job+submission[5]
FLIP-74: https://cwiki.apache.org/confluence/display/FLINK/FLIP-74%3A+Flink+JobClient+API[6]
FLIP-85: https://cwiki.apache.org/confluence/display/FLINK/FLIP-85%3A+Flink+Application+Mode