- Ruby / Rails関連
週刊Railsウォッチ: config.autoload_libとconfig.autoload_lib_onceが追加ほか(20230725前編)
こんにちは、hachi8833です。これ見てちょっと震えました。
マジでここ数年「冷夏」という概念が消失してて農作物的には良くても異常だよなぁ……と思ってしまう
人はさぁ、気温35℃超の多湿空間で長時間生存できるようにできてねえんだよ…………https://t.co/IyJwgsZvDf https://t.co/TLdgrSDfGJ pic.twitter.com/oLTPfB8aW8— 四号戦車🔞 (@Panzer_04) July 18, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
なお、更新情報のCVE-2023-28362については以下の記事でお知らせ済みです。
🔗 autoload_lib関連
🔗 config.autoload_lib
を追加
概要
このパッチは、lib
のオートロードやeager loadingを手軽に行えるAPIを導入する。# config/application.rb config.autoload_lib(ignore: %w(assets tasks generators))
経緯
lib
ディレクトリは初期のRailsバージョンではオートロードパスに含まれていた。しかしlib
ディレクトリにはさまざまな種類のファイルが格納されており、production環境でeager loadingすると問題になることがある。一部のファイル(ジェネレーターなど)はオートロードやeager loadingの対象にならない。
このため、Rails 3ではデフォルトのオートロードパスからlib
を削除していた。app/lib
lib
についてこれといったソリューションがなかったため、app/lib
を利用するアイデアが浮上した。新しい
app
のサブディレクトリは自動的にオートロードパスとeager loadパスに追加される。そして、後からそこに通常のコード(アセットやタスクなどではないコード)を配置するだけで、そのまま使えるようになる。ただし、自分は
app/lib
についていくつか指摘したい点がある。
app/models
やapp/controllers
は何らかの意味があるが、app/lib
は何も意味していない。これらは同じ階層にあるが、命名があまり一貫していない。- 単なる
lib
は本来アプリケーションコアから外れたものを指す。そこに置かれているものは、自分にとってはlib
よりもapp
の下に配置する方が適切と思える。- いずれにせよ、Railsプログラマーが
lib
からオートロードできるようにすべき。- 自分がコンサルティングしたほとんどのプロジェクトでは、手動でオートロードパスに
lib
を追加していた。誰もがlib
からオートロードしたいと思っており、そのことは理解できる。フレームワークでこのユースケースを基本機能としてサポートすべき。今は選択肢が増えた
Zeitwerkはファイルやディレクトリを無視できるので、Railsプログラマーは
lib
のオートロードやeager loadingを以下のようにもっといい形で制御できるようになっている。# config/application.rb module MyApp class Application < Rails::Application lib = root.join("lib") config.autoload_paths << lib config.eager_load_paths << lib Rails.autoloaders.main.ignore( lib.join("assets"), lib.join("tasks"), lib.join("generators") ) ... end end
技術的にはRails 6以降でできるようになっているが、今ならもっとよいAPIにできる。
config.autoload_lib(ignore:)
APIの設計では、例外的なユースケースには例外的なサポートがあってもいい。設計は一般的なケースを対象とし、エッジケースはエッジに任せればよい。
この場合、
lib
にはアドホックなAPIがふさわしいと考えている。これなら以下のようにユーザーが一括で設定できるようになる。# config/application.rb config.autoload_lib(ignore: %w(assets tasks generators))
ignore
に渡す値は、このAPIでlib
の外にあるものを無視しても無意味になるので、lib
に対して相対的になる。おまけに呼び出しも短く簡潔になる。引数をデフォルトにしない理由
これら3つのディレクトリをデフォルトにすべきかどうか考えたが、以下の例を見てほしい。
# config/application.rb config.autoload_lib
これでは、無視されているディレクトリが存在することが明確にならない。ファイルを開けば"autoload lib"が見えることになるが、このAPIは誤解を招いてしまう。自分は、クライアントにとって当然の選択肢ならデフォルト値にする意味があると思うが、この場合はそうは思えない。
この場合は、APIを明示的にして、どれを無視すべきかをユーザーに考えさせる形にする方が良いと思う。ドキュメントでも既にそうした例を示してユーザーをそちらに導くようにしている。
これを新規アプリケーションで生成しない理由
最終的にこのコードを新規生成されるアプリケーションに組み込むことは可能。ただし急いで進めたくないので、まずはAPIを公開して様子を見ることにしよう。問題なく動作するようなら、おそらくRailsの将来のバージョンで実現されるだろう。
これをエンジンで利用可能にしない理由
エンジンの
lib
ディレクトリは、一般にアプリケーションのlib
ディレクトリよりもむしろgemのlib
ディレクトリに近い。エンジンでのlib
再読み込みはもっと複雑になる。現時点では、自分が何をしているかをわかっている人が手動でオートローダーを設定するのが望ましいと思う。
同PRより
つっつきボイス:「Zeitwerk作者のfxnさんによるプルリクです」「Railsのオートロードといえばこの人」「ドキュメントも更新されてますね」
参考: 定数の自動読み込みと再読み込み (Zeitwerk) - Railsガイド
「今までも以下のようにRails.autoloaders.main.ignore
でディレクトリを除外できたのか↓: config.autoload_lib
にignore:
オプションを渡すことで同じことをできるようになったんですね」「assets/
やtasks/
やgenerators/
などは普通はRails実行時に必ずしも読み込む必要のないディレクトリで、その方がメモリも起動時間も節約できますね」
lib = root.join("lib")
config.autoload_paths << lib
config.eager_load_paths << lib
Rails.autoloaders.main.ignore(
lib.join("assets"),
lib.join("tasks"),
lib.join("generators")
)
参考: §6.5 オートロードパスにappを追加する -- Classic から Zeitwerk への移行 - Railsガイド
🔗 config.autoload_lib_once
を追加
この
config.autoload_lib_once
メソッドはconfig.autoload_lib
と似ているが、lib
をconfig.autoload_once_paths
に追加する点が異なる。
config.autoload_lib_once
を呼び出せば、lib
内のクラスやモジュールがオートロードされる。これはアプリケーションの初期化時でも呼び出せるが、再読み込みは行われない。
同PRより
つっつきボイス:「これもfxnさんのプルリクです」「今度はconfig.autoload_lib_once
: これは名前の通り一度だけ読み込まれて再読み込みされないんですね」
#railties/lib/rails/application/configuration.rb#467
def autoload_lib_once(ignore:)
lib = root.join("lib")
# Set as a string to have the same type as default autoload paths, for
# consistency.
autoload_once_paths << lib.to_s
eager_load_paths << lib.to_s
ignored_abspaths = Array.wrap(ignore).map { lib.join(_1) }
Rails.autoloaders.once.ignore(ignored_abspaths)
end
🔗 Action Mailboxでバウンスメールをbounce_now_with
で送信可能になった
動機/背景
このプルリクを作成した理由は、Active Jobとキューを経由せずにバウンスメールを(
ActionMailbox.bounce_with
で)送信しなければならないユースケースに最近遭遇したため。このユースケースに対応してもよさそうなので、バウンスメールの即時配信を可能にする代替メソッドbounce_now_with
の追加を提案する。詳細
このプルリクは、キューを経由しないバウンスメール送信をトリガーする
ActionMailbox::Base#bounce_now_with
を追加する。# バウンスメールをエンキューする MyMailbox.bounce_with MyMailer.my_method(args) # メールを即時配信する MyMailbox.bounce_now_with MyMailer.my_method(args)
同PRより
つっつきボイス:「これはAction Mailboxの改修」「Action Mailerの処理中にバウンスを検知した場合に行う処理のようですね」
参考: Action Mailbox の基礎 - Railsガイド
参考: バウンスメール - Wikipedia
「ところでAction Mailboxの機能を見ていると、Railsの中になつかしのqmailが入っているような気持ちになりますね」「そうそう、Action Mailboxは実質的にメール受信サーバーを作っているようなものですね」「今qmailを使っているところはさすがに減ったかな」
🔗 railties:install:migrations
にDATABASE
オプションを追加
これにより、
rails railties:install:migrations
を実行する際に、マイグレーションをどのデータベースにコピーするかを指定できるようになる。$ rails railties:install:migrations DATABASE=animals
Matthew Hirst
同Changelogより
つっつきボイス:「railtiesでマイグレーションをインストールしたことないかも」「使い道がよくわからないけどマルチプルデータベースかRailsエンジンあたりに関係していそうですね🤔」
マイグレーションをコピーする必要のあるエンジンが複数ある場合は、代わりに
railties:install:migrations
を使います。$ bin/rails railties:install:migrations
なお、rail tieは線路の枕木のことです。その名もRailtieというおもちゃがあるようです↓。
You're never too young to Tie One On... pic.twitter.com/4GupOBVKqc
— RailTieYardGame (@RailTie) July 6, 2017
🔗 SHA1ハッシュダイジェストで非決定論的に暗号化された従来のデータの復号をサポート
これにより、Active Recordの新しい暗号化オプションが追加され、SHA1ハッシュダイジェストで非決定論的に暗号化されたデータを復号できるようになる。
Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true
新しいオプションは、7.0から7.1にアップグレードする際の問題に対処する。Active Record暗号化の初期化方法にバグがあったため、非決定論的暗号化に用いるキープロバイダは、Railsが
Rails.application.config.active_support.key_generator_hash_digest_class
でグローバルに設定したものではなく、SHA-1をダイジェストクラスとして利用していた。
Cadu Ribeiro、Jorge Manrubia
同Changelogより
このプルリクは、SHA1ハッシュダイジェストで非決定論的に暗号化された既存のデータをサポートする新しいActive Record暗号化オプションを追加する。
現在、7.0から7.1にアップデートするユーザーにはActive Recordの暗号化に関する問題が存在する。#44873以前は、非決定論的な暗号化でデータを暗号化する場合、常にSHA-1が使われていた。その理由は、
ActiveSupport::KeyGenerator.hash_digest_class
がrailtieの設定内のafter_initialize
ブロックで設定されていたが、暗号化設定はそれよりも前の段階で実行されるようになっていたため、結果的に従来のデフォルト値であるSHA-1が使われていたため。つまり、既存のユーザーは非決定論的な暗号化にSHA256を使い、決定論的な暗号化にSHA1を使っていることになる。このプルリクで追加される新しいオプション
support_sha1_for_non_deterministic_encryption
は、ユーザーが既存データの復号でSHA1とSHA256の両方をサポートする目的で有効にできる。新しいオプションは、7.1より前のバージョンではデフォルトで有効になる。考え方は以下のとおり。
- 既存のユーザーは、7.0からのアップデート時にこのオプションを設定する必要はない(デフォルトで有効になるため)。
- 7.1以降のユーザーは、このオプションが無効になる。
これにより、Active Record暗号化の初期化システムがさらに堅牢になる。この初期化はすべてのイニシャライザが実行された後に実行されるようになり、フィルタリングする属性を宣言するシステムも異なる読み込み順で動作する。
徹底的な調査とデバッグを行ってくれた@duduribeiroにひたすら感謝したい。彼はこの問題を突き止めて修正する代替手段を模索してくれた(#48520参照)。私はそのプルリクから移植されたコミットの共同著者として彼を追加した。
同PRより
参考: Active Record と暗号化 - Railsガイド
参考: SHA-1 - Wikipedia
参考: SHA-2 - Wikipedia
つっつきボイス:「以前#44873でActive Recordの暗号化でダイジェストを変更可能になったことがあったけど(ウォッチ20230322)、非決定論的な暗号化ではkey_generator_hash_digest_class
で指定したものが効かずにSHA-1が使われていたのか」「Rails 7.0で非決定論的暗号化の場合に設定が食い違っていたんですね」「この問題を調査して対応方法をプルリクにまとめるのは大変そう...」
後で調べると、#48530に他にも追加修正が入っていました↓。
参考: Fix Active Record encryption not picking up encryption settings with eager-loading by jorgemanrubia · Pull Request #48577 · rails/rails
参考: Fix queries for deterministically encrypted attributed for data migrated from 7.0 by jorgemanrubia · Pull Request #48676 · rails/rails
🔗 Deprecation::Behavior
に:report
を追加
config.active_support.deprecation = :report
を設定すると、エラーレポーターがActiveSupport::ErrorReporter
に非推奨警告を報告するようになった。
非推奨警告は、重要度:warning
の処理済みエラーとして報告される。
production環境で発生する非推奨事項をバグトラッカーに報告するのに有用。
Étienne Barrié
同Changelogより
この振る舞いは、非推奨警告を重大度
:warning
で処理されたエラーとしてErrorReporter
で報告する。動機/背景
エラーレポーターは、production環境で発生するエラーを報告する方法として優れていて、"error"を処理済みとして宣言できる。アプリケーションのライフサイクルでは、新しいマイナーバージョンにアップグレードした後、非推奨機能は徐々に解決され、再導入を防ぐために完全に禁止されることもある。その後、再び使われたり警告が無視されたりしないよう、非推奨警告がdevelopment環境やtest環境で発生するように設定される。しかし通常のproduction環境では、非推奨警告が完全に抑制されている(config.active_support.report_deprecations
オプションで設定される)。次のマイナーバージョンにアップグレードする直前の最後のステップで、すべてのコードパスをカバーするテストの信頼性が万全ではない場合は、production環境で発生する非推奨事項をアプリケーションのバグトラッカーに報告できると便利だろう。
詳細
バックトレースなどをトラッキングして重複レポートを削除すべきかどうか悩んだ。最終的に複数のワーカープロセスが同じ非推奨事項を繰り返し報告するので、そこまでやってもさほど有用ではなく、実装が複雑になるだけだと思えた。
デフォルトでは、報告されたエラーは処理済みとマークされ、重大度
:warning
になる。自分は懸念が非常に低いことを示すために:info
にすることも考えたが、「deprecation warning」という用語に沿って、処理済みエラーのデフォルト値である:warning
にした。
同PRより
参考: ActiveSupport::Deprecation::Behavior
つっつきボイス:「これまで非推奨警告の振る舞いで指定可能だった:raise
、:stderr
、:log
、:notify
、:silence
に:report
も追加されたんですね: production環境のアプリケーションログにたくさん出てきそうだけど、バグトラッカーで問題を見つけたいという気持ちもわかるので、そういう場合にはよさそう👍」
参考: §3.14.10 config.active_support.deprecation
-- Rails アプリケーションを設定する - Railsガイド
前編は以上です。
バックナンバー(2023年度第3四半期)
週刊Railsウォッチ: Kaigi on Rails 2023プロポーザル募集、rubocop-magic_numbersほか(20230721後編)
- 20230719前編 複合主キー関連の実装進む、Action TextでHTML5サニタイザほか
- 20230705後編 AWS LambdaでRailsをRackで動かすLambyほか
- 20230704前編 productionのforce_ssl=trueがデフォルトで有効に、rakeタスクをthorで書くほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)