こんにちは。freee 会計の開発をしている chandai です。
この記事は freee Developers Advent Calendar 2023 の16日目の投稿になります。
早いもので今年も残り僅かですね。
私からは今年行った freee 会計の Zeitwerk モード移行について共有させていただきます。
目次
はじめに
freee 会計では現在 Rails 6.1 を使用しています。来たる Rails 7 系へのアップグレードに向けて、Rails のオートローダーを Zeitwerk モードに移行する必要がありました。
昨年行われた Rails 6.0 → Rails 6.1 へのアップグレードは以下の記事をご覧ください。
詳細な移行方法については Rails ガイド 様の Classic から Zeitwerk への移行 がとても分かりやすいため、本記事では上記ガイドに則り、移行に際して実施した修正作業について記載していきます。
事前準備
移行時の準備として、手元の開発環境で以下の修正を行いました。
- オートローダーを
classic
モードからzeitwerk
モードに変更
# config/application.rb config.autoloader = :zeitwerk
- ローカル環境の
config.eager_load
を true に
# config/environments/development.rb config.eager_load = true
bin/rails zeitwerk:check
実行時に非互換な箇所を全て出力するようローカルの gem にパッチをあてる
チェックコマンド実行時に非互換な箇所、または名前解決のできないコードに遭遇すると raise
ないし NameError
が発生してそこで止まってしまうため、
修正箇所の全体感を把握するためローカルに bundle install
した zeitwerk
gem に以下の修正を加えました。
# ../gems/zeitwerk-x.x.x/lib/zeitwerk/loader/callbacks.rb -- raise Zeitwerk::NameError.new(msg, cref.last) ++ puts Zeitwerk::NameError.new(msg, cref.last)
# .../gems/zeitwerk-x.x.x/lib/zeitwerk/loader/helpers.rb private def cget(parent, cname) parent.const_get(cname, false) end ↓ private def cget(parent, cname) begin parent.const_get(cname, false) rescue NameError puts "NameError: uninitialized constant #{parent}::#{cname}" end end
Zeitwerk 非互換な箇所の修正
bin/rails zeitwerk:check
を実行して検知された Zeitwerk 非互換なコードの修正について羅列します。
大文字略語(Acronym) の修正
CSVImporter、SQLBuilder といった大文字略語(Acronym)のクラス(定数)名は zeitwerk
モードによるクラスの推測が効かなくなるため CsvImporter、SqlBuilder のようなアッパーキャメルケースにするか、
下記の通り ActiveSupport:: Inflector
を用いて独自のルールを設定することができます。
# config/initializers/zeitwerk.rb Rails.autoloaders.each do |autoloader| autoloader.inflector.inflect("csv" => "CSV") autoloader.inflector.inflect("sql" => "SQL") end
私達としては基本的には公式の設定に準拠する方針であったため、特殊なケースを除いてはルールを設定せずアッパーキャメルケースにしていました。
一方で OmniAuth のように Gem 名がアッパーキャメルケースになっていないものをラップしているようなクラスでは、認知負荷を下げるためルールを設定してそのままにしました(omniauth はアッパーキャメルケースにすると Omniauth になります)。
ファイル名と定数名の不一致の修正
例として hoges_service.rb というファイル名にも関わらず HogeService というクラスが設定されている場合になります。
このようなケースでは bin/rails zeitwerk:check
実行時に以下のような分かりやすいメッセージが出力されるため、しらみ潰しに修正していました。
expected file /repository_name/app/services/hoges_service.rb to define constant HogesService, but didn't
また、以下のような既存のクラスを拡張したオープンクラスのファイルも同じく修正の対象になりました。
# app/models/user.rb class User ... end # app/models/users/foo.rb # MEMO: クラス名には Users::Foo が設定されるべきだがオープンクラスのためそうなっていない class User self.foo puts "bar" end end
こちらのケースではオープンクラスをモジュール化して、既存の挙動を壊さないよう対応しました。
# app/models/user.rb class User include Users::Foo ... end # app/models/concerns/users/foo.rb module Users::Foo extend ActiveSupport::Concern class_methods do def foo puts "bar" end end end
クラスのないファイルにクラスを追加
以下のような定数のみ置かれているファイルも修正の対象になりました。
# lib/constant.rb MAGIC_NUMBER = 100 # app/models/user.rb require 'lib/constant.rb' class User def number MAGIC_NUMBER end end
こういったファイルは require
して読み込んでいましたが、明示的に require
せず、以下のようにクラスを付与してオートロードの対象になるよう変更しました。
# lib/constant.rb class Constant MAGIC_NUMBER = 100 end # app/models/user.rb class User def number Constant::MAGIC_NUMBER end end
検証環境での動作確認中に起きた出来事
さて、一通り非互換な箇所の修正が完了し、動作確認のため検証環境にデプロイする際、一部の Gem で LoadError
が発生してしまいました。
rake aborted! LoadError: cannot load such file -- xxx ...
こちらの原因としては development
にのみ bundle install していた Gem を require していたファイルが eager_load により読み込まれてしまい、インストールされていない検証環境で LoadError が発生したものになります。
上記のような Gem の require は development 下でのみ行うよう修正して事なきを得ました。
おわりに
1つ1つの修正の難易度は高くないものの、修正範囲がとても多く、移行作業にはそれなりの時間を要しました。とはいえ細かい単位でマージ & リリースを重ねたおかげか本番環境への反映後も zeitwerk 起因によるバグは見つかっておらず、今では胸を撫で下ろすばかりです。
さて、freee Developers Advent Calendar も後半戦に入っていますが明日からもまだまだ続きます!
Developers だけでなく SRE さんが主体となった freee 基盤チーム、QA エンジニアの皆さんによる freee QA、デザイナーの皆さんによる freee Designers のアドベントカレンダーも並行して執筆されておりますので、こちらも併せてご覧ください。
それでは明日の更新もお楽しみに〜👋