- Ruby / Rails関連
週刊Railsウォッチ: Rails 7.0.5のcreate_association挙動変更取り消し、YJITの性能を最大限引き出す方法ほか(20230809)
こんにちは、hachi8833です。
お知らせ: 来週の週刊Railsウォッチはお休みをいただきます。
🔗Rails: 先週の改修(Rails公式ニュースより)
- 公式更新情報: Ruby on Rails — A much needed Active Storage documentation with a new option to trigger has_secure_token
- 公式更新情報: Ruby on Rails — This Week in Rails - July 28, 2023(前半)
🔗 コンソールでMessageVerifier#inspect
とKeyGenerator#inspect
の秘密情報が表示されないよう修正
動機/背景
コンソールでsecret(変数)にアクセスすると秘密情報を表示できてしまう。
inspect
メソッドをオーバーライドしてクラス名のみ表示するようにすることで、機密情報が誤って出力されないようにする。詳細
# 変更前 ActiveSupport::MessageVerifier.new(secret).inspect "#<ActiveSupport::MessageVerifier:0x0000000104888038 ... @secret=\"\\xAF\\bFh]LV}q\\nl\\xB2U\\xB3 ... >" ActiveSupport::KeyGenerator.new(secret).inspect "#<ActiveSupport::KeyGenerator:0x0000000104888038 ... @secret=\"\\xAF\\bFh]LV}q\\nl\\xB2U\\xB3 ... >"
# 変更後 ActiveSupport::MessageVerifier::Aes256Gcm(secret).inspect "#<ActiveSupport::MessageVerifier:0x0000000104888038>" ActiveSupport::KeyGenerator::Aes256Gcm(secret).inspect "#<ActiveSupport::KeyGenerator:0x0000000104888038>"
同PRより
つっつきボイス:「inspect
で情報が露出しないようにする改修はこの間もありましたね(ウォッチ20230719)」「前回はRails.application.config
が対象だったけど今回はActive SupportのMessageVerifier
とKeyGenerator
が対象なのか」「こういうのは値を直接参照すればもちろん見えるんですが、inspect
の結果は見るつもりがなくてもデフォルトでIRBに出力されてしまうので、こういうのを見つけたら対応するのがよいと思います👍」
参考: Rails API ActiveSupport::MessageVerifier
参考: Rails API ActiveSupport::KeyGenerator
🔗 トランザクションがreturn
、break
、throw
でコミットするようになった
修正: #45017
参考: #29333
参考: ruby/timeout#30かつて、エラーが発生した場合のみロールバックがトリガーされていた時代があったが、Ruby 2.3では
timeout
ライブラリが実行を中断するためにthrow
を使うようになり、オープン中のトランザクションがコミットされるという逆効果が生じていた。これを解決するために、Active Record 6.1ではトランザクションを(コミットではなく)ロールバックするように動作を変更していた(不完全なトランザクションをコミットする可能性よりも安全性が高いため)。
Rails 6.1以降は、transaction
ブロック内でのreturn
やbreak
やthrow
の利用は事実上非推奨となっていた。しかし、
timeout 0.4.0
のリリースにより、Timeout.timeout
で(throw
ではなく)再びエラーをraiseするようになった。これによって、Active Recordの振る舞いを当初の(驚きの少ない)動作に戻すことが可能になった。
同PRより
以下のコンフィグで、Rails 6.1より前の振る舞いをオプトインできるようになった。
Rails.application.config.active_record.commit_transaction_on_non_local_return = true
Rails 7.1で作成される新規アプリケーションではこれがデフォルトになる。
同Changelogより抜粋
つっつきボイス:「オープン中のトランザクションをthrow
でコミットできた時代があったとは知らなかった」「Rails 6.1ではトランザクション内でreturn
やbreak
やthrow
するなというのはよく言われてましたね」「この改修ではTimeout
の実装が変わったのを機に、トランザクション内でreturn
やbreak
やthrow
を行うとコミットするようになったのか」「この振る舞いはオプトイン可能なのね」「これはありがたいです🙏」
参考: library timeout
(Ruby 3.2 リファレンスマニュアル)
「そういえばトランザクション内にreturn
とかを書くとRuboCopで怒られた覚えあります↓」「そうそう、Railsを長くやっているとこのあたりが身体に染み付いていますよね」
参考: Rails/TransactionExitStatement
-- Rails :: RuboCop Docs
🔗 Active Storageで生じがちなissueの対応方法をガイドに追記
このプルリクは、Active Storageで調べる必要が生じがちな、いくつかの一般的な状況の対応方法をドキュメント化する。
has_many_attached
で既存の添付ファイルを保持する方法- フォーム送信がバリデーションに失敗した場合にアップロードされたファイルを保持する方法
同PRより
つっつきボイス:「Active StorageガイドにFAQが追加されたそうです」「既存の添付ファイルを洗い替えるかどうかの問題か、なるほど」
添付ファイルの置き換え vs 追加
Railsでは、デフォルトで
has_many_attached
関連付けにファイルを添付すると、既存の添付ファイルが置き換えられます。既存の添付ファイルを保持しながら新しい添付ファイルを追加する場合は、Rails.application.config.replace_on_assign_to_many
をfalse
に設定してください。または、以下のように個別の添付ファイルで
ActiveStorage::Blob#signed_id
を持つhiddenフォームフィールドを使います。<% @message.images.each do |image| %> <%= form.hidden_field :images, multiple: true, value: image.signed_id %> <% end %> <%= form.file_field :images, multiple: true %>
これは、既存の添付ファイルを選択的に削除可能になるというメリットがあります。たとえば、JavaScriptで個別の非表示フィールドを削除できます。
フォームのバリデーション
添付ファイルは関連するレコードの
save
が成功するまで、ストレージサービスに送信されません。つまり、フォームの送信がバリデーションに失敗すると、新しい添付ファイルは失われ、再度アップロードする必要があります。ダイレクトアップロードではフォームの送信前に保存されるので、これを使うことでバリデーションが失敗した場合でもアップロードが失われないようになります。<%= form.hidden_field :avatar, value: @user.avatar.signed_id if @user.avatar.attached? %> <%= form.file_field :avatar, direct_upload: true %>
同PR差分より
参考: Active Storage の概要 - Railsガイド
🔗 has_secure_token
を生成するタイミングを指定可能になった
動機/背景
モデルで
has_secure_token
を定義しても、その値を初期化では利用できない。詳細
has_secure_token
を拡張して、コールバックのタイミングを指定するon:
オプションを渡せるようにする(デフォルトは引き続きbefore_create
)。値が生成されるときのコールバックについて:
on: :initialize
を指定して呼び出すと、値はafter_initialize
コールバックで生成される。それ以外の場合は、値はbefore_
コールバックで使われる。デフォルトは:create
。
同PRより
つっつきボイス:「on
オプションが追加されたことで、モデルを永続化する前(データベースに保存する前)の段階でもhas_secure_token
を利用可能になったんですね」「デフォルトはbefore_create
なのか」「初期化後にhas_secure_token
にアクセスできるならバリデーションエラーに引っかからずに済みそう」「Action Cableなどで非同期な処理を行っているときなどに、こうやって永続化前にアクセスしたくなったりしますね」
🔗 RackミドルウェアのテストにRack::Lint
を導入
Rack::Lint
をRailsのミドルウェアテストに導入これは厳密にはユーザー向けのものではないが、Railsの将来を確保するためにRack SPECとの互換性を維持することが重要。
内容に興味があるか、Rackに依存しているライブラリを管理している場合は、Rack 3アップグレードガイドを参照。
Rails公式ニュース見出しより
つっつきボイス:「#48874自体はissueで、そこにRack::Lint
関連のマージ済みプルリクがたくさんリンクされていました」「これはRails 7.1で導入予定のRack 3に関連する改修でしょうね」
🔗 SchemaCache
のメンバーの値をダンプ時にソートするようになった
関連: #42717
このプルリクによって結果が一貫し、結果のダイジェストをキャッシュキーなどで利用できるようになる。
同PRより# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L280 def encode_with(coder) # :nodoc: - coder["columns"] = @columns - coder["primary_keys"] = @primary_keys - coder["data_sources"] = @data_sources - coder["indexes"] = @indexes + coder["columns"] = @columns.sort.to_h + coder["primary_keys"] = @primary_keys.sort.to_h + coder["data_sources"] = @data_sources.sort.to_h + coder["indexes"] = @indexes.sort.to_h coder["version"] = @version coder["database_version"] = @database_version end
つっつきボイス:「とてもシンプルなプルリクでした」「indexes
やcolumns
などをソート済みにしてダンプする、なるほど」「たしかにソートされていないと結果が不定になってダイジェストの値も変わってしまいますよね」「そうそう、SchemaCache
の中身を参照するテストが不安定になってしまうとかはありそう」
🔗 オーディオアナライザのメタデータにタグを自動追加するようになった
動機/背景
このプルリクを作成した理由は、mp3ファイルをアップロードしてTVデバイスに提供するRails APIを作成中のため。データを手動で入力する代わりに、ファイルを分析して'title'や'artists'などのタグメタデータオブジェクトにアクセスする必要がある。詳細
このプルリクは、オーディオアナライザが出力するメタデータハッシュに、blobのタグを含むtags
キーを追加する。
同PRより
つっつきボイス:「Active Storageの改修です」「音声データ機能にも何やかやで地道に機能が追加されていますね」
参考: §7 ファイルを解析する -- Active Storage の概要 - Railsガイド
🔗 副作用のないcapture_emails
とcapture_broadcasts
テストヘルパーを追加
- PR: Introduce
capture_emails
andcapture_broadcasts
by ghiculescu · Pull Request #48798 · rails/rails
参考: #47025 (comment)、#47793 (comment)
@dhhのフィードバックに対応し、
assert_emails
とassert_broadcasts
で副作用が起きないようにすることを検討している。
これらのプルリクはまだRailsのリリースに含まれていないため、breaking changeは問題ないと思う。
同PRより
つっつきボイス:「これはAction CableとAction Mailerの改修なんですね」「assert_emails
とassert_broadcasts
に副作用があるのは好きじゃないとDHHからフィードバックを受けて、Changelogも変更されている↓」「こういうテストを手書きするのは大変なので、テストヘルパーがあるのはいい👍」
# actioncable/CHANGELOG.md#L15
-* `assert_broadcasts` now returns the messages that were broadcast.
+* Introduce the `capture_broadcasts` test helper.
- This makes it easier to do further analysis on those messages:
+ Returns all messages broadcast in a block.
```ruby
- message = assert_broadcasts("test", 1) do
- ActionCable.server.broadcast "test", "message"
- end
- assert_equal "message", message
-
- messages = assert_broadcasts("test", 2) do
+ messages = capture_broadcasts("test") do
ActionCable.server.broadcast "test", { message: "one" }
ActionCable.server.broadcast "test", { message: "two" }
end
assert_equal 2, messages.length
assert_equal({ "message" => "one" }, messages.first)
assert_equal({ "message" => "two" }, messages.last)
```
*Alex Ghiculescu*
🔗Rails
🔗 Rails 7.0.5のcreate_association
の挙動変更が次のマイナーアップデートで取り消しに
revertコミット積まれたぞ、という内容を追記しました -
Rails 7.0.5以降におけるcreate_associationメソッドの挙動変更についてまとめ - おもしろwebサービス開発日記 https://t.co/mI4eM1qyUQ— willnet (@netwillnet) August 2, 2023
つっつきボイス:「この間取り上げたhas_one
のcreate_association
の挙動変更(ウォッチ20230721)を取り消すプルリクがマージされたそうです」「変更が破壊的すぎてつらいという声が上がったのかな」「取り消しのプルリクを見ると、いったん取り消してから仕切り直す流れになったようですね↓」「既存のテストがあちこちで壊れてたのか」「この変更の影響を受ける人は結構いそうだなと思ってたら本当にそうなった」「マイナーアップデートの変更にしては大きかったですよね」
この
7-0-stable
ブランチでは、#48425で追加されたテストと、#46790による元のbreaking changeを取り消す。これにより、#46737や#47554で期待される動作が壊れる代わりに、長年続いてきた単一関連付けにおける
create_record
の元の挙動が復元される。この変更により、uniqunessバリデーションが失敗するという報告が寄せられ(例:#48632や#48330)、稼働中のアプリケーションでも多くのテストが壊れた。これにより、#48643などの回避策が試みられたり、#48683で元の動作をサポートしないようにする取り組みが行われた。
これらの問題は解決が難しいが、@matthewdと協議した結果、いったん元に戻してから再構築するのがベストという結論になった。それが終われば、今後別のプルリクでこの振る舞いを議論できるようになる。
#48809より
🔗 Deviseの高度な使い方まとめ
つっつきボイス:「AppSignalの記事です」「DeviseでOmniAuth認証、DeviseでAPI認証、AuthtrailでDeviseログインをトラッキングのあたりを扱っている」「Deviseでパスキー認証もできるようになったらいいな〜」
「JWT(JSON Web Token)用のgemがちゃんとDeviseにあるのね: jwt_revocation_strategy
などが使えるのはえらい」「Authtrailは使ったことないけど、Deviseのログインをトラッキングするgemらしい: ログイン時のIPやuser agentなども記録できるのはよさそう」
「Deviseの基本に忠実というか教科書になりそうな内容ですね」「令和の年にふさわしいDevise記事👍」
参考: JSON Web Token - Wikipedia
🔗 Ruby 3.1->3.2でRailsアプリの挙動が変わった
つっつきボイス:「Rubyを3.2にしたらRailsアプリの挙動が変わるというのは自分も最近見かけた気がする」「CSVライブラリのバージョンが変わると挙動が変わっていたとは」「"Thread[]
とThread[]=
はThreadローカルではなくてFiberローカルな変数を扱っている"、へ〜!」「マルチテナント用ライブラリのapartmentは使ったことなかった」「gemでマルチテナントやったことないかも」「マルチテナントはコードを手書きすることは割りとあるかもしれませんね」
🔗 RSpecのWebdrivers::VersionError
対処方法
Qiita記事書きました。最新のselenium-webdriverを使うとwebdrivers gemが不要になるみたいです。
RSpecを実行するとWebdrivers::VersionErrorが発生する場合の対処方法 https://t.co/dqBlUqphb2— Junichi Ito (伊藤淳一) (@jnchito) August 1, 2023
「このWebdrivers::VersionError
、最近CIで見た気がする」「私もminitestで見た気がします」
🔗Ruby
🔗 Rubyワーカーのメモリ使用量を調査・改善
この手のライブラリが有りそうな気がするが、自作してRuby製 worker のメモリ使用量を調査して改善した。頑張った( ˘ω˘) https://t.co/1EjevHtFHE
— Watson (@watson1978) August 2, 2023
つっつきボイス:「Rubyワーカーのメモリ肥大化、こういうのはありそう」「RubyのObjectSpaceライブラリで調査したんですね」「ObjectSpaceを使えばメモリに関する情報はだいたい取れますね」
参考: module ObjectSpace
(Ruby 3.2 リファレンスマニュアル)
🔗 YJITの性能を最大限引き出す方法
Posted to Hatena Blog #はてなブログ
YJITの性能を最大限引き出す方法 - k0kubun's blog https://t.co/LhMELqBUX2— k0kubun (@k0kubun) August 1, 2023
つっつきボイス:「具体的な設定の目安や値まで書かれていて↓、とても有用な記事👍」
YJITを有効化すると、YJITが生成する機械語に加えて、それに関するメタデータもメモリを消費する。機械語の最大サイズは
--yjit-exec-mem-size
(デフォルト 64MiB) で制限されるが、メタデータは特にリミットがない。ただし、メタデータサイズは生成コードのサイズに比例する傾向にあり、かつRuby 3.2の時点ではメタデータは生成コードの3~4倍程度メモリを使うと見積っておくと良い2。従って、デフォルトではメモリは最大で256~320MiB使われることになる3。
同記事より抜粋
「言われてみればなるほど↓: forkした後にそれぞれがJITコンパイルされるのでコンパイル結果の共有は難しいでしょうね」「これはメモリを消費しそう」
ここで注意しなければならないのは、この値はあくまで各プロセスあたりのメモリ消費量であること。UnicornやPumaで複数プロセスを走らせる場合、ワーカーがforkする時点で存在しているメモリのページのうち、その後更新がされないものは複数プロセス間で共有される*4が、YJITのコードやメタデータに関しては基本的にワーカーのfork後に生成されるため、メモリの共有は期待できない。そのため、例えばUnicornのプロセスが16ある場合は、最悪の場合 16 x 256~320MiB = 4096~5120MiB 使うことを覚悟しなければならない。
同記事より抜粋(強調は編集部)
「UnicornからフォークしたPitchforkを使うと"Reforking"ができるのか↓」
同僚の@byrootがUnicornのフォークであるPitchforkというのを開発した。これはUnicornと比べてレガシーな依存が一つ外れているモダンなUnicornとしても使うことができるが、それに加えて "Reforking" と呼ばれている機能が追加されている。通常、UnicornやPumaのfork時にはアプリのコードがコンパイル済みでないワーカープロセスが作られるが、Reforkingというのはアプリのコードが既にコンパイルされているプロセスを後から定期的にforkし直すことでそのメモリを複数プロセス間で共有することを目指すというもの。YJITの運用でメモリの使用量を最適化しようとしたら、これが一番効果があると思われる。
同記事より抜粋
「YJIT関連でこんなにいろいろ起動オプションやビルドオプションがあるとは」「RubyでJITを手がけてきたk0kubunさんが書いているのが強い」「こういう記事を日本語で読めるのはありがたいですね」
同記事末尾で紹介されている関連記事もよさそうです↓
参考: Monitoring YJIT in Production | Rails at Scale
今週は以上です。
バックナンバー(2023年度第3四半期)
- 20230802前編 Active Storageバリアントの事前変換、Linkヘッダープリロードのオプトアウトほか
- 20230727後編 Rubyにdefp導入の提案、IRB 1.7.3リリースほか
- 20230725前編 config.autoload_libとconfig.autoload_lib_onceが追加ほか
- 20230721後編 Kaigi on Rails 2023プロポーザル募集、rubocop-magic_numbersほか
- 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ウォッチタグ)