Rails: Zeitwerk対応のためイニシャライザで大量のrequire_dependecyを削除(翻訳)
ある大規模なレガシーアプリに取り組んでいるときに、時代遅れのクラシックオートローダーから最新のZeitwerkオートローダーに乗り換えるという課題に直面しました。
ZeitwerkはRails 6ではオプションとなっていましたが、Rails 7からは必須になりました。
これまで使っていたRails 6では、新しいデフォルトのほとんどについてはどうにか対応しましたが、いよいよZeitwerkに切り替える時期が到来したと判断しました。
...しかし話はここから始まるのでした...
このコードベースで多くの時間を費やすうちに、require_dependency
を300回以上も呼び出す巨大なイニシャライザを1つ見つけました。
最初の兆候は、イニシャライザでリストされているファイルがすべてオートロード済みディレクトリの下に置かれていたことです。
公式のRailsガイドには以下のようにはっきり書かれています。
Zeitwerkによって、
require_dependency
の既知のユースケースはすべて削除されました。プロジェクトをgrepしてrequire_dependency
をすべて削除してください。
しかしrequire_dependency
を削除する前に、こんなファイルがある理由と背景を明らかにしておきたいと思いました。そのファイルの冒頭には、不安をかきたてるコメントが書かれていたのです。
# すべてのReportingモジュールをプリロードすること
# さもないと「uninitialized constant errors」が起きるかもしれない
#(主にrakeタスクで)
かなり怖いですよね?私も同感でした。production環境にNameErrorsを導入したい人などいるのでしょうか?もちろん私は違います。
実際には、Sentryを用いてこれらのエラートレースの一部をどうにか見つけることに成功しましたが、そのエラーをローカルで再現できなかったのです。そこで環境にどんな違いがあるのかを深掘りし始めました。
production.rb
ではeager loadingが有効になっていました。
これはパフォーマンス指向の環境では完全に定番の設定ですが、この設定がrakeタスクで効かないことがわかりました(参考)。
rakeタスクは、production環境で実行されるときにもdevelopment環境と同様に、コードベースをeager loadingしていなかったのです。-
productionのポッドは、DebianベースのあるLinuxディストリビューション上で動いており、ローカルのdevelopment環境はmacOSでした。
そのmacOSのファイルシステムは大文字と小文字を区別しないデフォルト設定になっていましたが、Linuxのファイルシステムは大文字小文字を区別する設定になっていました。
もうひとつ気付いた点は、謎のイニシャライザ内のファイルリストを見ると、lib/report/PL/X123/products
のように大文字小文字が通常なら使わないような形でパスに含まれていたことでした。
🔗 クラシックオートローダー
クラシックオートローダーでeager loadingを無効にすると、定数名をファイル名に変換するときにReport::PL::X123.to_s.underscore
が呼び出され、その結果report/pl/x123/products
が生成されました。
このマジックは、Module#const_missing
メソッド内で未定義定数を参照するたびに発生します(これはよく知られているmethod_missing
コールバックと似ています)。このconst_missing
メソッドはRuby標準の実装ではエラーになりますが、Railsはconst_missing
メソッドをオーバーライドして、オートロード済みのディレクトリの中からファイルを見つけようとします。
しかし、大文字と小文字を区別するファイルシステムではreport/pl/x123/products.rb
は存在しないのと同じになります。これが、起動時にすべてのコードベースでeager loadingを行わない限りproduction環境でNameErrorsが発生する理由の手がかりです(Railsでeager loadingを有効にすると、起動中にすべてのファイルをeager_load_paths
で読み込みます)。
🔗 大文字小文字を区別しないファイルシステム(macOS上のdevelopment環境)
❯ ls lib/report/PL/X123/products.rb
lib/report/PL/X123/products.rb
❯ ls lib/report/pl/x123/products.rb
lib/report/pl/x123/products.rb
🔗 大文字小文字を区別するファイルシステム(Linux上のproduction環境)
$ ls lib/report/PL/X123/products.rb
lib/report/PL/X123/products.rb
$ ls lib/report/pl/x123/products.rb
ls: cannot access 'lib/report/pl/x123/products.rb': No such file or directory
🔗 Zeitwerkで何が変わったか
Zeitwerkオートローダーの振る舞いは、これと逆です。
Zeitwerkは、オートロードされたディレクトリにあるファイルをリストし、個別のファイルで.delete_suffix!(".rb").camelize
を呼び出すことでファイル名を定数名に変換します。この変換では語形変化(inflection)ルールが考慮されてReport::PL::X123::Products
という結果を得ますが、ファイルシステムが大文字小文字を区別するかどうかには影響されません。
Zeitwerkは、Ruby組み込みの機能であるModule#autoload
を用いて、定数の読み込み元となるファイルを指定します。
# 起動時
autoload :Report, Rails.root.join('lib/report')
# Reportを最初に参照するとき
Report.autoload :PL, Rails.root.join('lib/report/pl')
# Report::PLを最初に参照するとき
Report::PL.autoload :X123, Rails.root.join('lib/report/PL/x123')
# Report::PL::X123を最初に参照するとき
Report::PL::X123.autoload :Products, Rails.root.join('lib/report/PL/X123/products.rb')
これをシンプルな言葉で説明すると以下のようになります。
Report::PL::X123::Products
に遭遇し、
それが定数テーブルにない場合は
lib/report/PL/X123/products.rb
を読み込む。
ここまで理解できたので、イニシャライザで延々と繰り返されるrequire_dependency
を自信を持って削除できました。作業はスムーズに終わり、NameErrorsは二度と発生しませんでした。
いずれにしろ、私は今後プロジェクトのファイルツリーで大文字を含むファイルを見かけるたびに、疑いのまなざしを向けるつもりです。
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
参考: Classic から Zeitwerk への移行 - Railsガイド