こんにちは、ujihisa といいます。現在スタディサプリのProduct Platform Team で Product Platform Engineerとして仕事を行っています。
Ruby 3.2
3ヶ月ちょっと前の2022-12-25 (JST) に、Ruby 3.2.0がリリース されました。2023-03-05現在の最新安定版はRuby 3.2.1です。
スタディサプリではRailsなどのwebアプリケーションが26個あり、それ以外も含めると全部で29個のRubyのプロジェクトがあります。記事執筆現在、これらの中で最も古いRubyバージョンは3.0.4です。
一般に、プロジェクトでは常に最新のバージョンのRubyが使えるべきで、当たり前ですが最新バージョンの新機能をうまく使っていって仕事を進めることができるのはとても気持ちいいですよね。逆に、最新バージョンが使えないならば、今後の運用に大きな影響を与えるので直ちに修正されるべきです。
アップデート対象
前提として、スタディサプリのRubyプロジェクトは大きく3つにわかれます。
- A) 複数のサービスから共有されるDBにアクセスするときに用いる専用の共有ライブラリ (例: "schema" gem)
- B) ^ に依存するモノリスのRailsアプリ
- C) 独立した一般のRailsアプリ
図解するとこんな感じです。Aはschemaとよばれるgemが主で、他にそれっぽいのがいくつかありますが、以下ここでは単にschemaとよびます。
schemaを用いたアプリであるBに属するものは、いわゆる分断されたモノリスというやつです。これが少なければよいのですが現実は非情でそんなに少なくはないです。だいたいownerとなるチームがあるんですが、歴史的事情により明確なownerがいない、境界線を跨いでどちらのownerが管理するべきか悩ましいものも残念ながらいくつかあります。
著者の属するチームは、いろいろ仕事があるんですが、そのうちの一つとして、どのチームからも使われる境界線上のライブラリであるAのschemaのRubyやRailsのバージョンアップを進めていく役割をもっています。これができないと、Bの各サービスのownerがそれぞれのものをアップデートできないですしね。
実際にやることは、schema gemの.ruby-versionを3.2.1などにあげて既存のrspecが通るかどうか確認しつつ、CI (GitHub Actions)で既存のRubyのバージョンのテストに加えて新しくRuby 3.2.1のテストも行うようmatrix testの設定を入れます。
Ruby 3.2へのアップデートのブロッカー (解決済み)
いくつかの罠にひっかかったので紹介します。
nokogiriのバイナリパッケージ
nokogiri-1.13.10-x86_64-linux
を用いているプロジェクトでは、単純にruby 3.1から3.2にアプデすることはできませんでした。ruby 3.2のためのnokogiriのバイナリパッケージがないためです。これはnokogiriのより新しいバージョンを使うことで、ruby 3.2用のバイナリパッケージがすでに存在するため、容易に解決可能です。
bundle update nokogiri
なお、バイナリパッケージを用いている理由は、nokogiriのためではなく、grpc gemを手元でビルドすることが依存ライブラリcares関係で不可能なためです。
third_party/cares/cares/ares_setup.h:32:10: fatal error: ares_config.h: No such file or directory 32 | #include "ares_config.h" | ^~~~~~~~~~~~~~~
endless rangeの挙動の変更
これはruby 3.2 release noteにかかれていないので正直不可解なのですが、実際に以下のような挙動を観測したので、仕方なく対処したものです。
#!ruby -v case Time.now when (nil..) p :ok end
このコードはruby 3.1とruby 3.2で異なる挙動をします。
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux] :ok
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux] /tmp/vW524c4/724:4:in `===': cannot determine inclusion in beginless/endless ranges (TypeError) from /tmp/vW524c4/724:4:in `<main>'
既存のコードでまさにこの挙動に依存していたものがあったため、以下のようにrangeの終端に必ずもとの値よりも大きい値がくるように設定することで、回避しました。
case Time.now when (nil..Time.now + 1) p :ok end
single line pattern matchingの仕様変更
以前のコードで
scope :locked_for_learner, -> learner_id: { where(finalized: false).where(learner_id: learner_id) }
のようにlambdaでkwargsの特定のkeyを取得するといったことをやっていましたが、ruby 3.2ではこれができないので、以下のように2行に書き換えました。
scope :locked_for_learner, -> (hash) { hash in {learner_id: learner_id} where(finalized: false).where(learner_id: learner_id) }
pg gem
PostgreSQLを用いるためのpg gemもアップデートする必要がありました。1.2.3では動作せず、1.4.5では動作します。 ちょっとこの作業をやったのがだいぶ前で、古いバージョンだと具体的にどのようなエラーになるのか忘れてしまった... なんだっけかなー
rspec-mocksのreceive
キーワード引数を用いたメソッドをrspecでmockしたときに、意図しない挙動になることがわかりました。
個人的にこれの問題解決が最困難でした。というのも、mockした箇所によって落ちたテストのエラーをみたときに、その原因となる箇所がかなり多岐に渡ってしまうためと、これによって引き起こされる現象がコードによって大きく異なるためです。
具体的には以下のような感じになります。
# 実装 module AbcClass def self.method_f(id:) ... end end # テスト expect(AbcClass).to receive(:method_f) do |**kwargs| expect(kwargs[:id]).to eq(xyz) end
現象:
expected: BSON::ObjectId('63f2a9569071303cfc9859c4') got: nil (compared using ==)
修正方法は、rspec-mocksのバージョンをあげることです。3.10.0では問題が発生し、3.12.3ならば解決しました。詳しくは https://github.com/rspec/rspec-mocks/pull/1514 にあります。間接的にRuby 3.2.0のkwargsまわりの変更の影響です。直接的ではないというのがわかりにくいポイントでした。
細かいところ
とある「CIではテストが通るのにローカルでは通らない」箇所があって、そこでかなり苦戦していました。が、よくよく調べてみるとruby 3.2にしなくても古いバージョンでももとから壊れていて、だれもその部分をCIではない開発環境でテスト実行していなくて誰も気づいてなかったというだけでした。ついでなのでここも修正して解決してすっきりしました。
補足
今回は小中高プロダクト基盤開発グループが普段の業務としてやっているアレコレのうちの一つである、社内RailsアプリなどのRubyのバージョンをあげる取り組みに関する便利情報を軽く共有しました。ポジションの募集要項 に、それ以外のあれこれが書かれていて、そちらのページも便利そうです。
画像がないとちょっぴり寂しいので、アイキャッチ画像としてつい先日たべたパフェの画像を貼ります。こちらはMinkというthe International Chocolate Awardsなどいろんな賞をとりまくっているチョコレートの名店で注文したものです。商品名はFrench vanilla yogurt, fresh fruit, honey almond granola, chocolate ganacheというfruit parfaitで、価格は$6.95 (ex. tax and tip)です。
文責: ujihisa