Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Zeitwerk対応のためイニシャライザで大量のrequire_dependecyを削除(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: Classic から Zeitwerk への移行 - Railsガイド

Rails: Zeitwerk対応のためイニシャライザで大量のrequire_dependecyを削除(翻訳)

ある大規模なレガシーアプリに取り組んでいるときに、時代遅れのクラシックオートローダーから最新のZeitwerkオートローダーに乗り換えるという課題に直面しました。

fxn/zeitwerk - GitHub

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は二度と発生しませんでした。


いずれにしろ、私は今後プロジェクトのファイルツリーで大文字を含むファイルを見かけるたびに、疑いのまなざしを向けるつもりです。

関連記事

Rails: Active RecordでRepositoryパターンを実装する(翻訳)

イベント設計のシンプルなヒューリスティック「誰が誰を呼ぶべきか?」(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。