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

週刊Railsウォッチ: Rails 7.0.5のcreate_association挙動変更取り消し、YJITの性能を最大限引き出す方法ほか(20230809)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

お知らせ: 来週の週刊Railsウォッチはお休みをいただきます。

🔗Rails: 先週の改修(Rails公式ニュースより)

🔗 コンソールでMessageVerifier#inspectKeyGenerator#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のMessageVerifierKeyGeneratorが対象なのか」「こういうのは値を直接参照すればもちろん見えるんですが、inspectの結果は見るつもりがなくてもデフォルトでIRBに出力されてしまうので、こういうのを見つけたら対応するのがよいと思います👍」

参考: Rails API ActiveSupport::MessageVerifier
参考: Rails API ActiveSupport::KeyGenerator

🔗 トランザクションがreturnbreakthrowでコミットするようになった

修正: #45017
参考: #29333
参考: ruby/timeout#30

かつて、エラーが発生した場合のみロールバックがトリガーされていた時代があったが、Ruby 2.3ではtimeoutライブラリが実行を中断するためにthrowを使うようになり、オープン中のトランザクションがコミットされるという逆効果が生じていた。

これを解決するために、Active Record 6.1ではトランザクションを(コミットではなく)ロールバックするように動作を変更していた(不完全なトランザクションをコミットする可能性よりも安全性が高いため)。
Rails 6.1以降は、transactionブロック内でのreturnbreakthrowの利用は事実上非推奨となっていた。

しかし、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ではトランザクション内でreturnbreakthrowするなというのはよく言われてましたね」「この改修ではTimeoutの実装が変わったのを機に、トランザクション内でreturnbreakthrowを行うとコミットするようになったのか」「この振る舞いはオプトイン可能なのね」「これはありがたいです🙏」

参考: 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_manyfalseに設定してください。

または、以下のように個別の添付ファイルで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に関連する改修でしょうね」

Rack 2-> 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

つっつきボイス:「とてもシンプルなプルリクでした」「indexescolumnsなどをソート済みにしてダンプする、なるほど」「たしかにソートされていないと結果が不定になってダイジェストの値も変わってしまいますよね」「そうそう、SchemaCacheの中身を参照するテストが不安定になってしまうとかはありそう」

参考: 暗号学的ハッシュ関数 - Wikipedia

🔗 オーディオアナライザのメタデータにタグを自動追加するようになった

動機/背景
このプルリクを作成した理由は、mp3ファイルをアップロードしてTVデバイスに提供するRails APIを作成中のため。データを手動で入力する代わりに、ファイルを分析して'title'や'artists'などのタグメタデータオブジェクトにアクセスする必要がある。

詳細
このプルリクは、オーディオアナライザが出力するメタデータハッシュに、blobのタグを含むtagsキーを追加する。
同PRより


つっつきボイス:「Active Storageの改修です」「音声データ機能にも何やかやで地道に機能が追加されていますね」

参考: §7 ファイルを解析する -- Active Storage の概要 - Railsガイド

🔗 副作用のないcapture_emailscapture_broadcastsテストヘルパーを追加

参考: #47025 (comment)#47793 (comment)

@dhhのフィードバックに対応し、assert_emailsassert_broadcastsで副作用が起きないようにすることを検討している。
これらのプルリクはまだRailsのリリースに含まれていないため、breaking changeは問題ないと思う。
同PRより


つっつきボイス:「これはAction CableとAction Mailerの改修なんですね」「assert_emailsassert_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の挙動変更が次のマイナーアップデートで取り消しに


つっつきボイス:「この間取り上げたhas_onecreate_associationの挙動変更(ウォッチ20230721)を取り消すプルリクがマージされたそうです」「変更が破壊的すぎてつらいという声が上がったのかな」「取り消しのプルリクを見ると、いったん取り消してから仕切り直す流れになったようですね↓」「既存のテストがあちこちで壊れてたのか」「この変更の影響を受ける人は結構いそうだなと思ってたら本当にそうなった」「マイナーアップデートの変更にしては大きかったですよね」

この7-0-stableブランチでは、#48425で追加されたテストと、#46790による元のbreaking changeを取り消す。

これにより、#46737#47554で期待される動作が壊れる代わりに、長年続いてきた単一関連付けにおけるcreate_recordの元の挙動が復元される。この変更により、uniqunessバリデーションが失敗するという報告が寄せられ(例:#48632#48330)、稼働中のアプリケーションでも多くのテストが壊れた。

これにより、#48643などの回避策が試みられたり、#48683で元の動作をサポートしないようにする取り組みが行われた。

これらの問題は解決が難しいが、@matthewdと協議した結果、いったん元に戻してから再構築するのがベストという結論になった。それが終われば、今後別のプルリクでこの振る舞いを議論できるようになる。
#48809より

追記(2023-08-10)

その後#48809はRails 7.0.7に含まれました。

Rails 7.0.7がリリースされました

🔗 Deviseの高度な使い方まとめ


つっつきボイス:「AppSignalの記事です」「DeviseでOmniAuth認証、DeviseでAPI認証、AuthtrailでDeviseログインをトラッキングのあたりを扱っている」「Deviseでパスキー認証もできるようになったらいいな〜」

「JWT(JSON Web Token)用のgemがちゃんとDeviseにあるのね: jwt_revocation_strategyなどが使えるのはえらい」「Authtrailは使ったことないけど、Deviseのログインをトラッキングするgemらしい: ログイン時のIPやuser agentなども記録できるのはよさそう」

waiting-for-dev/devise-jwt - GitHub
ankane/authtrail - GitHub

「Deviseの基本に忠実というか教科書になりそうな内容ですね」「令和の年にふさわしいDevise記事👍」

参考: JSON Web Token - Wikipedia

🔗 Ruby 3.1->3.2でRailsアプリの挙動が変わった

つっつきボイス:「Rubyを3.2にしたらRailsアプリの挙動が変わるというのは自分も最近見かけた気がする」「CSVライブラリのバージョンが変わると挙動が変わっていたとは」「"Thread[]Thread[]= はThreadローカルではなくてFiberローカルな変数を扱っている"、へ〜!」「マルチテナント用ライブラリのapartmentは使ったことなかった」「gemでマルチテナントやったことないかも」「マルチテナントはコードを手書きすることは割りとあるかもしれませんね」

influitive/apartment - GitHub

🔗 RSpecのWebdrivers::VersionError対処方法

「このWebdrivers::VersionError、最近CIで見た気がする」「私もminitestで見た気がします」

🔗Ruby

🔗 Rubyワーカーのメモリ使用量を調査・改善


つっつきボイス:「Rubyワーカーのメモリ肥大化、こういうのはありそう」「RubyのObjectSpaceライブラリで調査したんですね」「ObjectSpaceを使えばメモリに関する情報はだいたい取れますね」

参考: module ObjectSpace (Ruby 3.2 リファレンスマニュアル)

🔗 YJITの性能を最大限引き出す方法


つっつきボイス:「具体的な設定の目安や値まで書かれていて↓、とても有用な記事👍」

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の運用でメモリの使用量を最適化しようとしたら、これが一番効果があると思われる。
同記事より抜粋

shopify/pitchfork - GitHub

「YJIT関連でこんなにいろいろ起動オプションやビルドオプションがあるとは」「RubyでJITを手がけてきたk0kubunさんが書いているのが強い」「こういう記事を日本語で読めるのはありがたいですね」

YJIT: CRuby向けの新しいJITコンパイラを構築する(翻訳)

同記事末尾で紹介されている関連記事もよさそうです↓

参考: Monitoring YJIT in Production | Rails at Scale


今週は以上です。

バックナンバー(2023年度第3四半期)

週刊Railsウォッチ: Railsフラグメントキャッシュ経由の情報漏洩に注意ほか(20230803後編)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

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