Rubyの開発を支える技術

こんにちは、遠藤(@mametter)です。RubyKaigi Takeout 2020お疲れさまでした。

現在クックパッドには、フルタイムでRubyの開発をしている人が2人います(笹田と遠藤)。 それぞれ、Ruby 3の目標である並列性と静的解析の実現をメインミッションに据えて活動していますが、実はそれ以外にもRubyの開発を支えるための活動をいろいろやっています。

今回は、遠藤が関わっている範囲で、「Ruby開発者会議を支える技術」「Ruby開発のリモート議論を支える技術」「Rubyの品質を支える技術」についてざっと紹介してみます。

1. Ruby開発者会議を支える技術

Rubyに対する機能提案などの議論は、原則として、バグトラッカ上で行われます。 しかし、設計者であり最終決定権を持つmatzの多忙などの理由で、それだけでは議論が停滞してしまうのも事実です。 そこでRubyでは、開発促進のために月例で開発者会議を行っています。 私はここ数年、この会議を運営することにコミットしています。

会議のプロセス

毎月、次のことを行っています(検討以外はだいたいすべて私がやってます)。

  • 開催の約1ヶ月前に議題を募集するチケットを作る(例:先月のチケット)。
  • 開催の数日前、挙げられた議題をmarkdownにまとめ、有志のコミッタとともに事前検討をする(準備会)。
  • 会議当日、matzおよび有志のコミッタと本検討をする。結論の出た提案については、なるべくその場でmatzに回答してもらう。
  • 開催の数日後、議事録を清書して公開する(例:先月の議事録)。

コミュニケーション技術

会議当日のコミュニケーションには、クックパッド提供のzoomと、オンラインmarkdown共同編集ツールであるhackmdを使っています。 コロナ以前は東京近郊でのオフライン会場もありました(matzは大体リモート参加)が、今年は完全オンラインです。 アジェンダおよび議事録の共同編集はいろいろ試した末、「リアルタイムの共同編集ができ、markdownで書けて、コード断片も書きやすい」ということでhackmdに落ち着いています。

準備会

当日の会議は午後半日、約5時間をかけていますが、20件程度の議題をさばくには十分とは言えず、効率化が課題でした。 そのため今年から、数日前に有志での事前検討を行っています。 これにより、当日の参加者が誰も議題を理解していない、という非効率が回避できるようになりました *1 。 またこのフェーズで、事前に担当者に話題を振っておいたり、議題が曖昧なチケット・matz判断が不要そうなチケットに事前にフィードバックを返したりもします。 これにより、当日に議題をさばき切れないことはだいぶ減りました。

非互換の影響検討

仕様変更を議論する際には、かならず非互換の影響が議論されます。かつては互換性を気にしない言語とたびたび揶揄されていたRubyですが、現代ではかなり気を使うようになりました。

たとえば先日から、set.rbを組み込みにするという提案がたびたび議論されています(結論は出ていない)。 こういうとき、「トップレベルにSetクラスを自力定義している人は実際どのくらいいるか?」というような疑問が浮かびます。 このとき役に立つのがgem-codesearchです。

gem-codesearchがセットアップされているコミッタ用共用サーバにログインし、次のようにすることで、全最新版gemに対する高速grepができます。

$ gem-codesearch "^class Set$"
/srv/gems/ConstraintSolver-0.1/lib/extensions.rb:class Set
/srv/gems/GoNodes-0.0.1/lib/monkeypatch/set.rb:class Set
/srv/gems/Narnach-minitest-0.3.3/lib/set_ext.rb:class Set
/srv/gems/Ron-0.1.2/lib/ron.rb:class Set
/srv/gems/acapela-0.8.1/script/create_voices.rb:class Set
/srv/gems/annlat-0.0.1/lib/annlat/LaRuby.rb:class Set
/srv/gems/antlr4-0.9.2/lib/antlr4/base.rb:class Set
...

もちろん、この検索対象はあくまで公開gemだけなので、非公開アプリケーションのコードでどうかはわかりません。一方ここでコンフリクトする例が1つでも見つかったら諦めるというわけでもありません。それでも、非互換の影響の見積にはそうとう腐心しています。

なお、gem-codesearchはバックエンドにgoogle/codesearchを利用しています。codesearchはインデックスサイズに4GB制限があり、最近gemのコードサイズがこの制限に触れてしまったので、頭文字のアルファベット26文字+非アルファベット文字の27個にインデックスを分割するという延命をしたりしました(google/zoektも試しましたが、検索の起動がかなり遅く感じたので)。

クレジット:gem-codesearchはakrさんが開発し、hsbtさんがメンテナンスしています。コミッタ用共用サーバはRuby Associationのご提供で、gem-codesearchは私が管理しています。

dev-meeting-bot

議題を募集するチケットはコピペと手編集で作っていましたが、毎月となると、意外と重労働でした。 そこで、Slackボットを作りました。上述のチケットは、このボットにポンと作らせています。

他にも、最新の会議チケットを教えてくれる機能や、参加者にzoomやhackmdのURLをメールする機能が付いています。

クレジット:dev-meeting-botを含め以後紹介するSlackボットの多くは私が開発していますが、チケット作成機能は yebis0942 さんや znz さんのコントリビューションです。また、Herokuが計算機リソースを提供してくれています。

2. Ruby開発のリモート議論を支える技術

開発者会議以外でも、我々は日々Rubyの開発のための議論を行っています。 必要に応じてzoom(クックパッド提供)を使うこともありますが、ほとんどはSlack上での議論です。 そのためRuby開発者のSlackでは、議論を円滑にするためにいろいろなボットがうごめいています。 ざっとご紹介。

クレジット:Ruby開発者のSlackはRuby Associationにご提供いただいています。

all-ruby

all-rubyとは、これまでにリリースされたほぼすべてのruby(0.49から最新版まで)をビルドするスクリプト、およびその生成実行ファイルを集めたDockerイメージです。 みなさんのお手元でも、次のようにdocker pullして試すことができます。

$ docker pull rubylang/all-ruby
$ docker run --rm -ti rubylang/all-ruby
root@50d2785e9b39:/all-ruby# ./all-ruby -e 'puts "Hello"'
ruby-0.49             -e:1: syntax error
                  exit 1
ruby-0.50             -e:1: syntax error
                  exit 1
ruby-0.51             -e:1: undefined method `puts' for "main"(Object)
                  exit 1
ruby-0.54             -e:1:in method `puts': undefined method `puts' for "main"(Object)
                  exit 1
ruby-0.55             -e:1: undefined method `puts' for "main"(Object)
                  exit 1
...
ruby-0.76             -e:1: undefined method `puts' for "main"(Object)
                  exit 1
ruby-0.95             -e:1: undefined method `puts' for main(Object)
                  exit 1
...
ruby-1.0-961225       -e:1: undefined method `puts' for main(Object)
                  exit 1
ruby-1.0-971002       -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
...
ruby-1.0-971225       -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a0            -e:1: NameError| undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a1            -e:1: NameError| undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a2            -e:1: NameError:undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a3            -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
...
ruby-1.1a9            -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
ruby-1.1b0            Hello
...
ruby-2.7.1            Hello
root@50d2785e9b39:/all-ruby#

Kernel#putsが入ったのはruby-1.1b0からだとわかりますね。このように、Rubyの仕様変更やバグを議論しているとき、どのバージョンから変わったのかを調べるのに役に立ちます。

これをより手軽に・議論中に試すために、Ruby開発者のSlackには「all-rubyボット」がいます。

all-ruby-bot

ボットのソースコードは公開されているので、興味がある人は設置してみてください(dockerコマンドが起動できて、かつSlackからのWebhookが受けられる計算機が必要です)。

クレジット:all-rubyはakrさんが作り(akrさんによる発表資料)、dockerイメージはhsbtさんがメンテナンスしています。Slackボットは私が作っています。

rubyfarm

all-rubyはリリース版ごとの実行ファイルでしたが、コミットごとの実行ファイルをためたイメージも作っています。 それがrubyfarmです。

これは、rubyfarm-bisectというgemから使うことを想定しています。 git bisectは、挙動が変わった(エンバグした・バグ修正された)タイミングをコミット単位で特定できるツールですが、rubyを毎回ビルドするのは比較的大変でした。 rubyfarm-bisectを使うと、コミットごとにビルドではなくdocker pullで済むので、待ち時間が大幅に減り、試行錯誤も容易になります。自動git bisectはいろんな理由で失敗することで有名なので、ビルド失敗がない&試行錯誤がやりやすい、というのはだいぶストレスを下げます。

ただ、Docker Hubがストレージを制限する方針を発表したので、非公開ローカルのレジストリに移行する予定です。

commit-link

挙動変更のあったコミットを特定できたら、それについてSlackで議論をするでしょう。 そのとき、コミットハッシュをコピペするだけでは、読む人が自分でgit showにかけたり、GitHubのリンクをたどったりする必要があって、ストレスフルでした。

そこで、Ruby開発者のSlackにはcommit-linkというボットがいます。

commit-botは全発言をウォッチしていて、10桁以上のコミットハッシュっぽいものがあったらGitHubを見てコミットが実在するか確認し、発見したらリンクを貼ってくれます。 地味ですが、非常に便利です。

その他

他にも、標準添付ライブラリのメンテナを教えてくれる"who-is-maintainer"や、脆弱性報告を議論する専用チャンネルを作ってくれる"h1-channel-creator"など、いろいろなボットがRuby開発議論を支えています。

3. Rubyの品質を支える技術

最後に、Rubyの品質を高めるためのCIの活動について。

Rubyには、CIがたくさんあります。 私が把握している限りで、GitHub Actions、Travis、AppVeyor CI(Windows)、弊社笹田による独自CI、そしてrubyci.orgです。 このようになっているのは(歴史的経緯もありますが)テストの目的がいろいろあるためです。 たとえば笹田独自CIは、まれにしか発生しないハイゼンバグ(GCやVM最適化周りでしばしば発生する)を洗い出すために同じコミットを何度も繰り返しテストし続けます。

rubyci

rubyci.orgは、たぶんRubyで一番古くからあるCIです。

もともとは、chkbuildというRuby専用のテスト実行ツールがあり、有志が自分の環境でchkbuildを実行した結果をキュレーションしたのがrubyci.orgでした。 しかし現在では、通常のCIはGitHub ActionsやTravisで十分になったので、主な特徴が変わっています。

  • GitHub Actionsにないような、ややマイナーな環境もカバーしている
  • コミッタは(ほとんどの)テスト環境に直接sshログインでき、デバッグができる
  • テスト中に表示される警告数も監視している

とくに、再現性の低いバグと戦わなければならないRubyコア開発においては、2番目の特徴は重要になっています。

現在では多くのテスト環境はAWS EC2インスタンス上で動作しています。これらの環境をセットアップしたり、コミッタのアカウントを自動定義したりするために、mitamaeを使った自動化もなされています(ruby/ruby-infra-recipe)。

クレジット:chkbuildはakrさんが作り、rubyci.orgはnaruseさんが作りました。その後のメンテナンスは主にhsbtさんと私が行っています。mitamaeによるプロビジョニングはhsbtさんが整理しました。AWSの費用はRuby Associationがカバーしてくれています。他にも、一部の環境はご提供を受けています(rubyci.org末尾のスポンサーリストを参照のこと)。

alerts-emoji

CIの結果を監視するのは苦行なので、みんなSlackなどへの通知を活用していると思います。 しかしRubyには多数のCIがあり、それぞれ好き勝手なフォーマットで通知を投げていたので、そのチャンネルを眺めて理解するだけでも苦行となっていました。

また、テスト実行に数時間かかる環境などもあるため、ちょっと古いコミットに対する通知が遅れてくることが多く、一体どのコミットに対する通知なのかパッと見でわからない、という問題もありました。

そこで、すべての通知を集約し、統一フォーマットで通知するようにしました。 また、どのコミットに対する通知かを視覚的にわかりやすくするため、コミットに適当な幾何学図形を割り当てることにしました。

なお、絵文字については非常に議論がありました。 私は10桁程度の16進数をパッと見で比較するのが苦手 *2 なので、ランダムに選んだ絵文字を振るようにしました(絵文字のほうが視覚的に比較しやすい)。 しかしそうすると、絵文字から意味を読み取ってしまう人から、逆に認知コストが上がったという反対の声があがりました(たとえばスマイルマークや㊙のように意味のある絵文字)。 議論と試行錯誤の末、色違いの幾何学図形の絵文字を使うようにすることで、だいたいの人が満足できるようになりました。

クレジット:まず笹田が独自CI用の通知チャンネルを作りました。そのチャンネルに各種通知が参入して破綻したので、この統一チャンネルを遠藤が作り、その後k0kubunさんがいい感じに整理してくれました。

まとめ

Ruby開発は、多数の技術、および(自分を含めた :-)多数の人々によって支えられています。

ここに上げたものはあくまで遠藤がかかわっているものの一部だけで、上げきれなかったものもいろいろあります(すみません)。みなさんに感謝しながらやっています *3

*1:正確には、去年も私がすべての議題を理解するように努めていたのですが、丸一日消費してしまうことに加え、一人では議題を誤解したり、論点を洗い出せなかったりして、いまいちだったので、笹田と相談して準備会にトライすることになりました。

*2:ハッシュの桁数が、GitHub Actionsは7桁、rubyciは10桁、というように通知ごとにバラバラだったのが特に最悪でした。

*3:特にCIのAWS費用がなかなか大変なので、その費用をカバーしてくれているRuby Associationに寄付をいただけるとありがたいです :-)

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/