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

週刊Railsウォッチ: Rails 8でKamalがデフォルトのデプロイツールになるほか(20240529)

こんにちは、hachi8833です。Rails 7.2.0のマイルストーン↓は、先週ぐらいには残り3つだったのが7つになったり5つになったりと変動しつつ、さっき見たら残り2つになっていました。

参考: 7.2.0 Milestone

週刊Railsウォッチについて

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

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

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

🔗 index_errorsnested_attributes_orderモードを追加してバグを修正

  • 関連付けバリデーションのエラーでindex_errorsのインデックス生成が誤っていたのを修正。

lulalala

  • index_errors: :nested_attributes_orderモードを追加。

ネステッド属性のセッターで受け取った順序に基づいて関連付けバリデーションエラーのインデックスを作成し、reject_if設定を尊重するようになる。これにより、APIはバリデーションエラーをフォームの個別のフィールドに対応付けるのに十分な情報をフロントエンドに提供可能になる。

lulalala
同Changelogより

  1. 以下の関連付けバリデーションエラーのバグを修正
    index_errors does not consider indexes of correct existing sub records #24390
  2. index_errors: :nested_attributes_orderモードを追加し、エラーの順序を変更可能にした

動機/背景

GitLabではindex_errorsを使っているが、バグを回避しなければならない(issueは#24390)。 このバグは、関連付けレコードの不完全なコレクションからインデックスが算出される可能性があるために、不正なインデックスが含まれる関連付けのバリデーションエラーに関係している。これを修正するには、関連付けの完全なコレクションからインデックスを作成する。

@tijwelchが最初に#24728を作成したが、@markedmondsonによってreject_ifが正しく動作しないと指摘された。また、議論の結果、「インデックス作成(indexing)」の意味付けに以下の2つの異なる解釈があることが判明した。

  1. (データベースの順序に基づいて)関連付け順序のインデックスを作成する。これは現在の振る舞い。
  2. インデックスをネステッド属性の順序で作成する(reject_ifはここにしか適用できない)

この2つの目標は互いに相反している。この両方に対応するために、このプルリクではnested_attributes_orderという新しい順序付けモードを追加している。このモードは、GitLabのユースケース(フロントエンドがネステッド属性を任意の順序で渡す可能性があるが、その順序に一致するエラーインデックスが引き続き必要)により適している。

詳細

このプルリクで追加する新しいActiveRecord::Associations::NestedErrorクラスは、インデックスの算出を処理する。元のインデックスロジックはAutosaveAssociationにあったが、このクラスに移動した。インデックス作成をassociation.target順にするかネステッド属性順にするかは、index_errors設定に基づいて選択する。

ネステッド属性順序については、nested_attributesが(roles_attributes=などのセッターで)代入されると、それに対応するレコードの配列がnested_attributes_targetとして関連付けオブジェクトに保存される。続いて、NestedErrorがこの配列にアクセスしてインデックスを算出可能になる。reject_ifも動作する(何かがrejectされた場合はnested_attributes_targetにプレースホルダとしてnilが置かれるので、インデックス全体が維持される)。
同PRより


つっつきボイス:「GitLabからのプルリクなんですね」「Rails 5.0のときに以下のプルリクでindex_errors: trueを指定可能になっていた↓けど、今回のプルリクでネステッド属性かどうかで振る舞いを変えるためにindex_errors: :nested_attributes_orderも指定可能になったのね: 特殊な要件に見えるけど実は一般性の高いニーズかもしれない」

参考: Errors can be indexed with nested attributes by mprobber · Pull Request #19686 · rails/rails

🔗 SKIP_TEST_DATABASE_TRUNCATE環境変数でマルチプロセスのテスト実行を高速化可能になった

ENV["SKIP_TEST_DATABASE_TRUNCATE"]フラグを追加。これは、すべてのテストがデフォルトのトランザクション内で実行される、巨大なDB上のマルチプロセステストをスピードアップする。

これによりHEYでは、178個のテーブルに対して24個のプロセスで実行されるテストを最大10秒短縮できた(最大4000個のテーブルのTRUNCATEがスキップ可能になったおかげ)。

同PRより


つっつきボイス:「DHHによるプルリクです」「TRUNCATEしなくてもいいテーブルをスキップすることでテストを高速化した: 普通ならdatabase_cleaner gemを使うところかなと思うけど、DHHならこうやってRailsに反映する方が話が早いでしょうね」「修正もとてもシンプルですね」

# activerecord/lib/active_record/tasks/database_tasks.rb#L386
      def reconstruct_from_schema(db_config, format = ActiveRecord.schema_format, file = nil) # :nodoc:
        file ||= schema_dump_path(db_config, format)
        check_schema_file(file) if file

        with_temporary_pool(db_config, clobber: true) do
          if schema_up_to_date?(db_config, format, file)
-           truncate_tables(db_config)
+           truncate_tables(db_config) unless ENV["SKIP_TEST_DATABASE_TRUNCATE"]
          else
            purge(db_config)
            load_schema(db_config, format, file)
          end
        rescue ActiveRecord::NoDatabaseError
          create(db_config)
          load_schema(db_config, format, file)
        end
      end

DatabaseCleaner/database_cleaner - GitHub

🔗 リクエストログのアロケーションカウントをGCタイムに置き換えた

アロケーションカウントはパフォーマンスにおいて興味深い指標になることが多いが、スレッドごとのメトリクスではないため、リクエストログに含める関連性が必ずしも高いとは限らず、マルチスレッド環境ではレポートが広範囲にわたって不正確になる。

Ruby 3.1からは、GCで消費した時間を単調増加するカウンタであるGC.total_timeが追加された。これは実際はスレッドごとのメトリクスではないが、レスポンスタイムと単位が同じになるのでより興味深い値となり、GCが一時停止したときのパフォーマンス問題がいつ発生したかを確認しやすくなる。

同PRより


つっつきボイス:「以前のUnicornでマルチプロセス・1スレッドで動かしているような環境ならリクエストログにアロケーションカウントを出力する意味はあると思うけど、マルチスレッドで動くPumaがRailsのデフォルトになった現代だとアロケーションカウントは微妙になってくるというのはわかる」

# actionpack/lib/action_controller/log_subscriber.rb#L36
-       additions << "Allocations: #{event.allocations}"
+       additions << "GC: #{event.gc_time.round(1)}ms"

🔗 ドキュメント: 「生成されるDockerfileはproduction用です」

# railties/lib/rails/generators/rails/app/templates/Dockerfile.tt#L3
+# Note: This Dockerfile is optimized for production deployment and isn't a good base
+# for development enviroments.

つっつきボイス:「プルリクのタイトルだけでわかる内容」「自分もrails newで生成されるDockerfileを開発用に使っていいのかどうかが気になってたんですが、おかげでスッキリしました😋」「開発用のDockerコンテナは例のdevcontainer(ウォッチ20240228)でやっていく流れっぽいですね」

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)

🔗 RAILS_ENVが未設定の場合にminitestが正常に動作するよう修正

Minitestは、インストール済みのgemをすべて自動的にスキャンしてからプラグインをそれらのgemから読み込む。Railsアプリのコンテキスト内で実行されているかどうかを検出して、その場合にのみMinitestの振る舞いを変更しなければならない。

bin/rails経由で実行されているかどうかを判断するには、RAILS_ENVが設定済みかどうかを確認するだけでよい。

Ref: minitest/minitest#996
Ref: minitest/minitest#725

同PRより


つっつきボイス:「tenderloveさんによる修正です」「コードがbin/railsで実行されたものかどうかを判定するにはRAILS_ENVが設定済みかどうかをチェックすればいいのか!」「Railsの中の人だけが知ってる情報という感じですね」

参考: 4 Rails環境の設定 -- Rails アプリケーションの設定項目 - Railsガイド

🔗 関連付けのquery_constraints:オプションが非推奨化された

関連付けのquery_constraintsオプションが非推奨化された。今後はforeign_keyを使うこと。

Nikita Vasilevsky
同Changelogより

このプルリクは、関連付けのquery_constraints:オプションに非推奨警告を追加する。このオプションは今後のRailsバージョンで振る舞いが変更されるため、アプリケーションは現在の振る舞いを維持するためにforeign_key:に切り替えることが推奨される。

関連issue: #49671 (comment)

非推奨化メッセージを出力するタイミング

現在のRailsは非推奨警告を起動時に出力するが、query_constraintsによるリフレクションが利用されている場合に限って実行時に非推奨警告を出力するよう変更するオプションがある。
自分がこれをinitializeに置いた理由は、可能なソリューションの中で最もシンプルだからに過ぎない。これを実行時に移動するには少しリネームが必要になる。
この非推奨警告を実行時に出力する理由があると思う方や、あるいは起動時と実行時の両方で出力すべきと思う方は知らせて欲しい。

テスト用モデルについて

この非推奨警告がテストで表示されないようにするため、すべてのモデルをforeign_key:に移行したが、長期的な目的は、Sharded::名前空間モデルでquery_constrainstsCpk::の新しい振る舞いを用いてforeign_keyを利用し続けることである。

また、CIがパスすればforeign_keyが実際にquery_constraints:を安全に置き換えられることが保証される。
同PRより


つっつきボイス:「issueを見ると、query_constraints:foreign_keyの振る舞いを切り離す処置の一環ということみたい: query_constraints:オプションを使ったことあったかどうかはすぐ思い出せないけど、いずれにしろ非推奨化されるのであればチェックは必要でしょうね」

参考: 4.3.2.13 :query_constraints -- Active Record の関連付け - Railsガイド

🔗 active_job.queue_adapterの設定がすべてのテストに反映されるよう修正

すべてのテストがactive_job.queue_adapterコンフィグを尊重するようになった。

修正前は、config/application.rbconfig/environments/test.rbconfig.active_job.queue_adapterを設定していても、選択したしたアダプタがすべてのテストで共通して使われるとは限らなかった。テストでは、指定したアダプタが使われることもあればTestAdapterが使われることもあった。

Rails 7.2では、queue_adapterコンフィグが設定されていればそれを尊重するようになった。設定が提供されていない場合はTestAdapterが使われる。

詳しくは#48585を参照。

Alex Ghiculescu
同Changelogより


つっつきボイス:「queue_adapterで別のアダプタを指定したのにTestAdapterが使われることがあったというバグを修正した、なるほど」「プルリクメッセージがみっちり書かれていますね↓」

動機/背景
config/application.rbconfig/environments/test.rbconfig.active_job.queue_adapter = 何らかのアダプタを設定しても、一部のテストケースで反映されないことがある。

具体的には、ActionDispatch::IntegrationTestActionMailer::TestCaseActiveJob::TestCaseでテストアダプタがTestAdapterに設定され、それ以外のテストケースではInlineAdapterに設定される。

ある環境にテストアダプタを設定すれば、そのテストアダプタが環境内のどこでも利用可能になることが期待されるだろう。たとえば、test環境でDelayed Jobアダプタを使うと、テストコードをproduction環境にさらに近づけることが可能になる。#37270のフィードバックもこれに倣っているが、特定のクラスでテストアダプタを無効にするという回避策は信頼できないことが示唆されている。

詳細

ジョブで使われるキューアダプタを決定するロジックは非常に込み入っている。

以下は現在のmainブランチでの動作。

  1. テストを実行するときに、そのテストのクラスがActiveJob::TestHelperincludeしており、かつqueue_adapter_for_testをオーバーライドしているのであれば、queue_adapter_for_testのアダプタが使われる。
  2. そうでない場合: テストを実行するときに、そのテストのクラスがActiveJob::TestHelperincludeしてる場合は:testアダプタが使われる。
  3. そうでない場合: ジョブクラスでself.queue_adapterが設定されている場合は、そのアダプタが使われる。
  4. そうでない場合: ジョブのスーパークラス(ApplicationJobなど)でself.queue_adapterが設定済みの場合は、そのアダプタが使われる。
  5. フォールバックする。
  6. ユーザー設定がない場合は、Rails.application.config.active_job.queue_adapterはデフォルトの:asyncになる。
  7. Railsのコンフィグが未設定の場合(Active Jobが単独で使われる場合など)は:asyncにフォールバックする。

上に記したように、ActiveJob::TestHelperは組み込みテストクラスの一部でしかincludeされていない。しかも、特定のジョブクラスでのみキューアダプタを指定する状況はめったにない(通常、異なるバックエンドで異なるジョブが実行されるのは望ましくない)。つまり実際には、development環境やproduction環境ではオプション5が使われる。test環境では、テストの種類に応じてオプション2または5が使われる。

このプルリクは、ロジックを以下のように変更する。

  1. ジョブクラスにself.queue_adapterが設定されている場合は、そのアダプタが使われる。
  2. そうでない場合: ジョブのスーパークラス(ApplicationJobなど)でself.queue_adapterが設定済みの場合は、そのアダプタが使われる。
  3. テストを実行したときに、そのテストのクラスがActiveJob::TestHelperincludeしており、かつqueue_adapter_for_testをオーバーライドしているのであれば、queue_adapter_for_testのアダプタが使われる。
  4. フォールバックする。
  5. ユーザー設定がない場合は、Rails.application.config.active_job.queue_adapterは、Rails.env.test?がtrueならデフォルトで:testに、そうでなければデフォルトの:asyncになる。
  6. テストを実行するときに、そのテストのクラスがActiveJob::TestHelperincludeしており、かつRailsのコンフィグが未設定の場合(Active Jobが単独で使われる場合など)は、:testが使われる。
  7. それ以外の場合は、:asyncにフォールバックする。

重要な変更点は以下のとおり。

  • これによって、ジョブクラスにqueue_adapterが設定されていれば常にその設定が尊重されるようになる。

  • queue_adapter_for_testのオーバーライドも引き続き有効だが、特定のジョブクラスに設定されたキューアダプタよりも優先されなくなった。どちらもめったに使われないと思えるし、この変更の影響はtest環境にとどまるので、比較的安全な変更だと思われる。

  • Railsコンフィグのデフォルトキューアダプタが環境に応じて変わるようになった(test環境なら:testに、それ以外はすべて:asyncに)。

デフォルトのRails環境テンプレートは、production環境でのみ特定のキューアダプタを設定することが示されている。これに該当するユーザーにとって、このプルリクで振る舞いは変わらない。

より細かな指定を行っているユーザーの場合は、ジョブキューアダプタごとに設定すれば、このプルリクで期待通りの振る舞いを引き続き得られるはず。

実際の変更は、すべての環境でデフォルトのキューアダプタを設定しているユーザーが対象となる(#37270)。このようなユーザーにとっては、指定のキューアダプタが一部のテストで使われるが他のテストでは使われないという問題がこのプルリクによって解決される。

追加情報

修正: #37270

参考: bensheldon/good_job#846
参考: #26360 -- このプルリクはここでの振る舞いを変更するが、自分にはやりすぎのように思える

内部問題を修正するため、この作業中に他のいくつかのプルリク(#48623#48626)から抽出した。
また、このプルリクの前に#48599からも議論の余地のない修正として抽出した。
同Changelogより

🔗 Rails 8からKamalがデフォルトのデプロイツールになる

デプロイにKamalをデフォルトで利用するようになり、Rails固有のconfig/deploy.ymlも生成するようになる。
--skip-kamalでスキップ可能。詳しくはhttps://kamal-deploy.org/を参照。

DHH
同Changelogより


つっつきボイス:「これもDHHによるプルリクですね」「お、Rails 8からはデフォルトでKamalでデプロイするようになるのか: 自分のように既にECSやCodeDeploy等でRailsアプリをデプロイする他の方法を確立済みの人たちにはKamalは不要かなと思いますけど、--skip-kamalでスキップできるようですし、そうでなくても手動で消せば済むので別にいいかな」

Kamal README: 37signalsの多機能コンテナデプロイツール(翻訳)


前編は以上です。

バックナンバー(2024年度第2四半期)

週刊Railsウォッチ: Ruby/Railsのアップグレード情報をscrapboxに集約ほか(20240514後編)

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

Rails公式ニュース


CONTACT

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