Skip to content

Ruby on Rails PR Digest - 2026年 9月

このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。

#58673 Refactor more class variables to accessors and delegators

マージ日: 2026/9/5 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    このPRは、Rails内部で使われていたクラス変数ベースの設定(mattr_accessor)をインスタンス用のattr_accessor等に置き換え、Ruby 4.0.6 でも Ractor から安全に参照できるようにするリファクタリングです。主に Action Pack / Action View の各種ヘルパの設定値の持ち方が変わり、それに対応するテストが追加されています。

  1. 変更内容の詳細

背景

  • Ruby 4.0.6 では @@var のようなクラス変数が Ractor セーフではない挙動をする一方、Ruby の edge 版では改善されている。
  • Rails 内部では設定やフラグを mattr_accessor(クラス変数ベースのアクセサ)で持っている箇所が多く、Ractor 利用時の安全性に問題が出る可能性がある。
  • このPRは、そうした「クラススコープの設定」を、Ractor からも安全に扱えるように、クラス変数ではない形(attr_accessor + 適切な保持先)に移行する一環。

主な修正ポイント

※ファイルレベルの要約です。実際のコードは概ね以下のような方向の変更になっています。

1. ActionDispatch::Http::URL の設定まわり

変更ファイル: actionpack/lib/action_dispatch/http/url.rb

  • URL 生成時に使う各種設定(例: default_port, asset_host 的なURL関連設定)を保持するクラスレベルの状態管理から、インスタンスアクセサ経由の管理に移行。

  • もともと以下のような形式だったと考えられます(疑似コード):

    ruby
    class ActionDispatch::Http::URL
      mattr_accessor :some_config, default: true
    end

    これが、クラスインスタンス変数 + class << self; attr_accessor ... end など、@@ を通らない構造になっている形です。

  • 変更行数を見ると +8/-9 と小さな差分なので、「アクセサ定義の仕方を変えたが、外部から見たAPIはそのまま(ActionDispatch::Http::URL.some_config=などで触れる)」というリファクタリングである可能性が高いです。

  • テスト: actionpack/test/dispatch/url_generation_test.rb に +19行
    → URL 生成が Ractor 上でも問題なく動く、あるいは設定値が期待通りに反映されることを確認するテストが追加されています。

2. Action View 各種ヘルパの設定

対象ファイル:

  • actionview/lib/action_view/helpers/content_exfiltration_prevention_helper.rb
  • actionview/lib/action_view/helpers/form_helper.rb
  • actionview/lib/action_view/helpers/form_tag_helper.rb
  • actionview/lib/action_view/helpers/navigation_helper.rb

ここでは、以下のような「グローバルなヘルパ設定」を担っていそうなフィールドが対象になっています。

例(代表的なパターンのイメージ・疑似コード):

ruby
module ActionView::Helpers::FormHelper
  # 以前:
  mattr_accessor :default_form_builder, instance_writer: false, default: SomeBuilder

  # 以後(イメージ):
  class << self
    attr_accessor :default_form_builder
  end
  @default_form_builder = SomeBuilder
end

あるいは、モジュールに対して mattr_accessor でクラス変数を払い出していたものを、cattr_accessor 的な形やモジュール固有のインスタンス変数 + attr_accessor に替えているはずです。

想定される具体例:

  • FormHelper / FormTagHelper
    • default_form_builder や、field_error_proc 等の「フォーム全体に影響する設定」をクラス変数からクラスインスタンス変数へ移行。
  • NavigationHelper
    • ナビゲーションレンダリング時の設定(例: current_page? 判定に関するオプションや、リンクの生成ポリシー等)に関連した設定変数が変更されている可能性があります。
  • ContentExfiltrationPreventionHelper
    • CSP的な出力エスケープ・コンテンツの持ち出し防止用のフラグ・設定クラス変数を、Ractor セーフな形へ移行。

3. Ractor + 各ヘルパ向けのテスト追加

変更ファイル:

  • actionview/test/template/base_ractor_test.rb (+10)
  • actionview/test/template/form_helper_test.rb (+23)
  • actionview/test/template/form_tag_helper_test.rb (+23)
  • actionview/test/template/navigation_helper_test.rb (+11)

これらは、

  • Ractor 内からヘルパを呼び出した場合にも、
    • 設定値が参照できる
    • 競合が起きない
    • 例外が発生しない
  • 従来通りのグローバル設定をいじるようなケースでも(FormHelper.default_form_builder = ...等)、正常に動作する

といった動作保証を行うテストが中心になっていると考えられます。

base_ractor_test.rb は Action View の Ractor 対応テストのベースクラス/共通化のための試験コードが追加されている可能性が高いです。


  1. 影響範囲・注意点

公開APIレベルでの影響

  • PRの内容・差分規模からすると、利用者のコードから見たAPI(メソッド名や使い方)はほぼ変わらないリファクタリングであると考えられます。
    • たとえば ActionView::Base.field_error_proc = ...ActionView::Helpers::FormHelper.default_form_builder = ... のような設定コードはそのまま動作するはずです。
  • ただし、クラス変数 (@@xxx) に直接アクセスしていたり、Rails の内部実装前提のハックをしている場合は注意が必要です。
    • 例: ActionView::Helpers::FormHelper.class_variable_get(:@@default_form_builder) のようなコードは動かなくなっている可能性が高いです。

スレッド / Ractor セーフティ

  • Ruby 4.0.6 で Ractor を利用しているアプリケーションにとっては、
    • Action Pack / Action View 内部でクラス変数を共有していた部分が減り、Ractor 間での不正共有や例外の発生可能性が下がります。
  • Ractor ごとに別々の設定値を持ちたい、という要件まではこのPRのスコープ外で、
    • あくまで「現状の『グローバル設定』モデルを維持しつつ、Ractor が問題なく使えるようにする」ことが目的です。

パフォーマンス・挙動変化の可能性

  • クラス変数からクラスインスタンス変数への移行は、通常はほとんどパフォーマンス差は出ません。
  • ただし、クラス階層をまたいでクラス変数の共有に依存していた場合(親クラス/モジュールで定義した@@varを子クラスも共有するようなパターン)は、
    • クラスインスタンス変数化により「親と子で別々の値になる」可能性が出てきます。
  • Railsの設計上、そのような継承前提の使い方を避ける方向に寄せる意味でも、この変更は理にかなっていますが、もし独自にヘルパを継承していて内部変数を見ている場合は挙動確認をした方が安全です。

  1. 参考情報 (あれば)
  • PR本体: https://github.com/rails/rails/pull/58673
  • 関連概念:
    • mattr_accessor は Rails のメタプログラミングヘルパで、内部的にクラス変数 @@ を使うため、Ractor では問題になることがある。
    • Ractor セーフな状態共有を行うには、
      • クラスインスタンス変数 + スレッド/Ractor 安全なオブジェクト
      • もしくはイミュータブルな定数・コピーオンライト前提の値
        などを使う必要がある。
  • Ractor とクラス変数の問題点は、Ruby 本体の Issue/Changelog にて継続的に議論されており、edge Ruby では徐々に改善中であるものの、Rails 側では保守的にクラス変数依存を減らす方向で対応している流れと見られます。

#58640 Allow to access view paths in a Ractor

マージ日: 2026/9/4 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    RailsアプリケーションをRactor対応(並列実行)した際に、「ビューのテンプレート探索(view paths lookup)」ができない問題を修正し、Ractor内からもビューのパスにアクセスできるようにした変更です。具体的には、Ractorで共有できなかったビュー関連キャッシュを「Ractor-shareable」になるように扱う(freezeする)ことで、Ractor内からのテンプレート検索を可能にしています。

  1. 変更内容の詳細

背景・問題点

  • Ractor内でテンプレートをレンダリングしようとすると、テンプレート探索処理が内部で参照するビュー関連のキャッシュが「Ractor-shareable」ではないためエラーになる、もしくはアクセスできませんでした。
  • Railsのビューシステム(Action View)は、テンプレート探索結果などをキャッシュして高速化していますが、このキャッシュオブジェクトがミュータブルな状態だとRactor間で共有できません。
  • Ractorでは「凍結された(freezeされた)オブジェクト」など、一部の条件を満たすオブジェクトだけが共有可能です。

このPRでの方針

  • 「Ractor化(ractorize)されたアプリケーション」では、ビュー関連のキャッシュを凍結してRactor-shareableにする、という割り切った実装を採用しています。
  • これは「eager load が有効な環境」を前提にしています。理由は、lazy load/autoload 中にコントローラが append_view_paths などでキャッシュを書き換える可能性があるためです。

PRの説明中の例:

ruby
class MyController < ApplicationController
  append_view_paths("some/folder") # ここでビューのパスに関するキャッシュが更新される
end

このようなコードが autoload のタイミングで実行されると、Ractor化した後にキャッシュがミュータブルなまま書き換えられてしまう可能性があります。そのため、

  • 「eager load を完了させてから」
  • 「ビュー関連のキャッシュを freeze して」
    Ractor実行を開始する、というフローを前提にしています。

具体的なコード変更(ファイルごとの概要)

※行数から読み取れる範囲での性質・目的を説明します。

  1. actionview/lib/action_view/path_registry.rb (+11/-0)
  • View paths のレジストリ(ActionView::PathRegistry)周りに、Ractor対応のための変更が入っています。
  • 典型的には以下のようなことをしている可能性が高いです:
    • 内部のキャッシュ構造(たとえば @cache のようなハッシュや配列)を凍結するメソッドの追加
    • Ractor対応用のメソッド(例: ractorize! など)で、パスレジストリ内のオブジェクトを再構成/コピーして shareable にする、あるいは freeze する
  • これにより、ActionView が持つビュー探索キャッシュが Ractor からも参照可能な形になります。
  1. railties/lib/rails/application.rb (+1/-7)
  • Railsアプリケーション全体をRactor対応するための「ractorize」処理の中で、ビュー関連キャッシュの扱いを変更しています。
  • 行数減から推測すると:
    • 以前は ractorize の中で別の(より大きな)処理をしていたのを簡略化し、View Path Registry 側の新しいAPI(freeze処理など)を呼び出すように整理した可能性があります。
    • もしくは、複数箇所でやっていたキャッシュの調整を一本化した、または eager load 前後のタイミングを整えた変更です。
  1. railties/test/application/ractors_test.rb (+16/-0)
  • Ractor内からビューにアクセスできることを検証するテストが追加されています。
  • 典型的には次のような流れのテストが考えられます:
    • テスト用のアプリケーションを eager load する
    • アプリケーションを Ractor 用に初期化(ractorize)する
    • Ractor を起動し、その中でコントローラまたはビューをレンダリングするコードを実行
    • エラーなくテンプレートが見つかりレンダリングできるかを確認

このテストにより、「Ractor内でのテンプレート探索が実際に成功すること」が自動的に保証されるようになっています。


  1. 影響範囲・注意点

影響範囲

  • 影響するのは「Ractor を使って Rails アプリケーションを実行し、Ractor 内でビューをレンダリングするケース」です。
  • 通常の(シングルRactor、マルチスレッド)運用のみであれば振る舞いはほぼ変わらない想定です。
  • Action View のパスレジストリやビュー探索キャッシュの内部実装に依存しているコードがあれば、freeze による影響(変更不能になること)を受ける可能性はありますが、普通のアプリケーションでは直接触ることはまれです。

注意点

  1. eager load が前提

    • Ractorでビューを使いたい場合、config.eager_load = true などにして、アプリケーションとビュー関連クラスがあらかじめ eager load されている必要があります。
    • 開発環境のように autoload / lazy load に頼っている設定では、
      • コントローラが autoload されるタイミングで append_view_paths などを実行し、
      • それが freeze 後のキャッシュに対して変更を試みる
        といった問題が起きる可能性があります(これはPR中でも「非 eager loaded 環境はまだ最適化の対象外」と明言)。
  2. append_view_paths などの動的変更との相性

    • Ractor化後(キャッシュ freeze 後)に append_view_paths のような API を使うと、
      • 例外になる
      • あるいは無視される
        など、挙動に制約が出る可能性があります。
    • Ractor運用を想定するなら、ビューのパス構成は起動時に確定させ、実行時に追加/変更しない設計が望ましいです。
  3. Ractor対応はまだ実験的な領域

    • PR本文からも、「まずは eager load 環境に絞って現実的な解決を入れた」というスタンスが読み取れます。
    • 非 eager load 環境でRactorを使うと、今回のビューキャッシュ以外にも同様の問題(ミュータブルなグローバル状態)が多数あると考えられるため、Ractorを本番導入する場合は十分な検証が必要です。

  1. 参考情報 (あれば)
  • 関連しそうなRails内部コンポーネント:
    • ActionView::LookupContext … ビュー探索の中心クラスで、path set や template cache を扱う
    • ActionView::PathRegistry … ビューのパス集合とそれに紐づくキャッシュを管理するクラス(本PRで直接変更された部分)
  • Ractorとshareableオブジェクト:
    • Ruby 3系のRactorでは、Ractor.shareable?(obj) が true を返すオブジェクトのみをRactor間で共有できます。
    • freeze されたオブジェクト(およびそこから辿れるオブジェクトも適切に freeze 済みであること)が基本的な要件になるため、「キャッシュをfreezeしてRactor-shareableにする」という方針はRubyのRactor設計に沿った妥当なものです。
  • 今後の展望:
    • 非 eager load / 開発環境でのRactorサポートをきちんと行おうとすると、autoloader(Zeitwerk)や各種キャッシュ、グローバル設定を広範に見直す必要があります。
    • このPRは「まずはRactorでもビューが使えるようにする」という第一歩的な位置付けで、Ractorサポート全体の中では一部機能の解消に相当します。

#58659 Fix Ractor unshareable procs at boot

マージ日: 2026/9/4 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Rails のブート時に生成されるいくつかの proc / lambda が Ractor 的に「共有不可能(unshareable)」になっていた問題を、キャプチャするローカル変数を整理することで「共有可能(shareable)」にする修正です。Ractor ワーカー利用時の IsolationError を避けつつ、ルートリローダーなど一部の「どうしても共有できない値」は Ractor 非対応機能として扱う方針を明示しています。

  1. 変更内容の詳細

背景: Ractor と shareable / unshareable オブジェクト

Ruby 3 の Ractor では、Ractor 間で自由に共有できるのは「shareable」にマークされたオブジェクトだけです。
Proc / lambda も、そのクロージャがキャプチャしている値に「unshareable」なオブジェクト(例: ミューテックス、IO、通常のオブジェクトグラフなど)が含まれていると Ractor.make_shareable できず、Ractor ワーカー起動時などにエラーになります。

この PR では、

  • Rails のブートプロセスで定義される複数の lambda / proc
  • ブロック内で不要に「unshareable なコンテキスト」をキャプチャしてしまっていたのを
  • 「必要なローカル変数だけを明示的に取り出してキャプチャする」パターンに統一

することで、Ractor 対応を強化しています。


主なパターン: 「使う値だけローカルに退避してから lambda を定義」

ほとんどの変更は同じリファクタリングパターンです。

典型パターン(抽象化)

修正前(イメージ):

ruby
some_setup do
  # ここで self や外側のコンテキストをそのままキャプチャする
  callback = -> { do_something_with(self.config) }
  register_callback(callback)
end

修正後(イメージ):

ruby
some_setup do
  config = self.config  # shareable な値だけ抜き出す
  callback = -> { do_something_with(config) }
  register_callback(callback)
end

ポイント:

  • self やレシーバに紐づいた巨大なオブジェクトグラフをキャプチャせず
  • shareable な値(例えば、freeze された設定値やシンボル、文字列、単純な Hash など)だけをローカル変数に退避し、それを lambda 内で参照
  • これによって callback 自体を Ractor.make_shareable できるようになる

このパターンが、以下のファイルで適用されています。

  • actioncable/lib/action_cable/engine.rb
  • activejob/lib/active_job/railtie.rb
  • activerecord/lib/active_record/associations/builder/association.rb
  • activerecord/lib/active_record/belongs_to.rb
  • activerecord/lib/active_record/secure_token.rb
  • railties/lib/rails/application/finisher.rb
  • railties/lib/rails/engine.rb

各所とも、ブート時に登録される初期化処理・コールバック・フックなどを、

  • 「Ractor 的に安全な lambda/proc」
  • かつ「グローバルなブート時に一度だけ定義されるもの」

として扱えるようにしています。

ActiveRecord::Associations / belongs_to まわり

belongs_to や関連ビルダーでは、関連付けの設定やオプションをクロージャでキャプチャしておき、後からコールバック内で使うパターンがよくあります。この PR では、そのキャプチャ対象を最小限のローカル変数に絞り、Ractor で共有しやすいように整理しています。

ActiveRecord::SecureToken

has_secure_token などで使われるトークン生成ロジックも lambda/proc で保持されることが多く、ここでも同様に「共有可能な値だけをキャプチャする」ように変更されています。たとえば:

  • 対象カラム名
  • 指定された長さ
  • 生成メソッド

などをローカル変数として抜き出し、その変数だけをクロージャ内で参照するようにすることで、トークン生成ロジックの proc が Ractor で安全に共有可能になります。

Routes reloader の特別扱い

説明文にもある通り、ルートのリローダーだけは他とパターンが異なります。

  • 「ルートリローダーが保持する状態」自体が Ractor 的に unshareable
  • よって、その値をキャプチャした Proc を shareable にすることは不可能

このため:

  • ルートリローダーに関しては「unshareable な値は reloader オブジェクトに保持させる」
  • proc 側からは reloader を使って処理するが、proc 自体は shareable にするのが困難
  • Ractor ワーカーからこのブロックを呼び出した場合は IsolationError が起きうる

ただし、現状の前提として:

Ractor workers are only supported for apps that don't enable reloading, and those don't mount the reloader middleware at all.

  • Ractor ワーカーは「コードリロードを有効にしていないアプリ」でしかサポートしていない
  • その場合は reloader middleware 自体をマウントしない=問題の proc が Ractor から呼ばれない

という実運用上の制約があるため、「ルートリローダーは Ractor 非対応で OK」という整理になっています。


テスト (railties/test/application/ractors_test.rb)

railties/test/application/ractors_test.rb にテストが追加され、以下を検証しています:

  • Rails アプリケーションを Ractor ワーカー対応の構成で起動したとき
  • ブート時に定義される各種 proc / lambda が Ractor.make_shareable 可能であること
  • #58655 と 88f486fc18 のコミットに依存している(これらがないとテストが通らない)

このテストが「どの初期化フックが Ractor shareable であるべきか」の仕様を事実上固定し、今後のリグレッションを防ぐ役割を担っています。


  1. 影響範囲・注意点
  • 影響を受けるのは主に Ractor を使うアプリケーション
    • Ractor ワーカーを利用していない通常の Rails アプリでは、挙動の変化はほぼありません(クロージャの書き方が変わっただけで、ロジックは同じ)。
  • Ractor ワーカー + コードリロード有効 は依然として非サポート
    • ルートリローダーは unshareable なままなので、Ractor ワーカーとコードリロードを同時に有効にする構成は、引き続きサポート外です。
    • その場合、reloader に関わる処理が Ractor 内で実行されると IsolationError が発生し得ます。
  • 独自の Railtie / Engine / 初期化処理を書く場合の実務的注意
    • Ractor を見据えている場合、今回の PR のパターンを真似するのが安全です:
      • initializerto_prepare などで lambda / proc を定義するときは、
        • selfRails.application などを丸ごとキャプチャしない
        • 必要な情報だけローカル変数に抜き出し、その変数だけをクロージャ内で参照する
      • shareable でないオブジェクト(DB コネクション、ミドルウェアインスタンスなど)は Ractor 間で共有しない前提で設計する

  1. 参考情報 (あれば)

#58669 Fix broken link to Redirecting Requests in layouts_and_rendering.md

マージ日: 2026/9/4 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails ガイド「Layouts and Rendering」の中で、存在しないアンカー #redirecting-requests への自己リンクがあったため、正しいガイド「Action Controller Overview」の該当セクションへのリンクに修正した PR です。これにより rake guides:lint のリンクチェックが再び通るようになっています。

  1. 変更内容の詳細
  • 問題点

    • guides/source/layouts_and_rendering.md 内で
      markdown
      [Redirecting Requests](#redirecting-requests)
      のように同一ファイル内の見出し #redirecting-requests を参照していました。
    • しかし、layouts_and_rendering.md にはそのセクションが存在せず、実際には action_controller_overview.md の中にあるため、rake guides:lint 実行時に以下の警告が出ていました:
      text
      [WARN] BROKEN LINK(s): layouts_and_rendering.md: #redirecting-requests
  • 修正内容

    • 上記の自己参照リンクを、別ガイド「Action Controller Overview」の該当アンカーへのクロスリンクに変更しています。
    • 具体的には、リンク先を以下のように修正した形です(実際の一部抜粋例):
      diff
      - See [Redirecting Requests](#redirecting-requests) for more details.
      + See [Redirecting Requests](action_controller_overview.html#redirecting-requests) for more details.
    • 同じファイル内の後ろの方には、すでに action_controller_overview.html#redirecting-requests へのクロスガイドリンクが存在しており、それと揃える形になっています。
  • 検証

    • ローカルで以下のコマンドを実行し、いずれも exit code 0 で通ることを確認済み:
      bash
      rake guides:lint:check_links
      rake guides:lint:mdl

  1. 影響範囲・注意点
  • 影響範囲

    • 変更対象はガイド文書 (guides/source/layouts_and_rendering.md) のみで、Rails の実行コードや挙動には一切影響しません。
    • Rails ガイドを HTML 生成したときに、「Redirecting Requests」へのリンクが正しく Action Controller Overview ガイド内の該当セクションに飛ぶようになります。
    • CI(Buildkite など)で rake guides:lint を実行している場合、#57152 以降に発生していたリンク切れによる失敗が解消されます。
  • 注意点

    • もし他ガイドから #redirecting-requests を layouts_and_rendering 側に向けてリンクしている箇所があれば、同様に action_controller_overview.html#redirecting-requests に統一する必要があります(この PR はそのうち 1 箇所のみを修正)。
    • 将来的にレイアウト/レンダリングのガイドに「リダイレクト」に関する独自セクションを追加する場合は、アンカー名の重複やリンク先の混在に注意する必要があります。

  1. 参考情報 (あれば)

#57152 [RF-DOCS] Rewrite the Layouts and Rendering guide

マージ日: 2026/9/4 | 作成者: @ayushn21

  1. 概要 (1-2文で)
    Layouts and Rendering ガイドがほぼ全面的に書き直され、内容の重複を整理しつつ「コントローラとビューの関係」を中心とした構成に再編されました。これに伴い、レイアウト構造やパーシャルなどの説明は Action View Overview へ移動され、リクエストバリアントの説明は Action Controller Overview から本ガイドへ移されています。

  1. 変更内容の詳細

2.1 ガイド全体の再編方針

  • 目的:
    • Layouts and Rendering ガイドが他ガイドと内容重複しており、話があちこちに飛ぶ構成になっていたため、「コントローラとビューの関係」に主眼をおいて書き直し。
  • 主な再配置:
    • Layouts and Rendering ガイドから削除/移動
      • パーシャルの詳細な説明 → Action View Overview に移動
      • レイアウトの構造、yield/content_for、アセットタグヘルパーなど「ビュー寄り」の話 → Action View Overview に移動
    • Action Controller Overview から移動
      • Request variants のセクション → Layouts and Rendering ガイドへ移動
        (レンダリングの話とまとめることで、より自然な流れに)

これにより、

  • Layouts and Rendering: 「コントローラがどうレスポンスを作り、どのビュー・レイアウトを選ぶか」にフォーカス
  • Action View Overview: 「ビューやレイアウトの書き方、構造、ヘルパー」にフォーカス
    という役割分担が明確になりました。

2.2 Layouts and Rendering ガイドの主な内容整理

PR 本文や内部レビューコメントから読み取れる主なポイントです。

2.2.1 コントローラとビューの関係を中心に再構成

  • コントローラの説明を、初心者にもわかるように修正:
    • 以前: “heavy code を Model に渡す” という曖昧な表現
    • 修正方針: “より複雑なロジックはモデルのメソッドへ委譲する” といった表現へ
  • 「HTTP レスポンスとは何か」「それを作る3つの代表的な方法」が明示される方向:
    • render で HTML や JSON などボディを含む完全なレスポンスを返す
    • redirect_to別 URL にリダイレクトするレスポンスを返す
    • headヘッダのみのレスポンスを返す
  • render の説明:
    • “full response” の意味を初心者向けに言い換え(例: ステータスコード + HTTP ヘッダ + レスポンスボディをブラウザに返すこと)
  • head の説明:
    • 「なぜヘッダだけ返すのか?」という動機例を入れる方向
      • 例: head :okhead :no_content (204)、head :not_found など API エンドポイントや条件チェックに使う場面

2.2.2 レンダリングの詳細 (render, redirect_to, head)

  • 「Using render」セクションを独立したトップレベルセクションに格上げする提案が反映されている可能性が高いです。
    • 同列に「Rendering by Default」「Using redirect_to」「Using head」といったセクションが整理され、サイドバーに出るように構造改善。
  • render に関して:
    • render オプション説明の整理 (template, partial, json, xml, inline など)
    • inline: について:
      • 有効なオプションとして示しつつも、**「通常はあまり使うべきでない」**ことを警告ボックスで明示。
      • もし使うとしても「小さなデバッグや非常に単純なレスポンス」などに限る、というトーンに。
    • JSON/XML レンダリング (render json:, render xml:) のセクションが重複気味だったため、1つのセクションに統合して説明する方針。
  • ステータスコードと render:
    • status: :unprocessable_entity のような例について、
      • 「テンプレートをレンダするときに任意のステータスコードを付けられる」
      • 「なぜそうするか(検証エラーなど)」に軽く触れ、
      • 詳細は後段の HTTP ステータス・MIME 関連の節へのリンクで補足するよう整理。

2.2.3 MIME タイプとレスポンス形式

  • MIME Content-Type の説明に対して、MDN Web Docs などへの外部リンクを付ける提案があり、ガイド内から参照しやすくなっています。
  • JSON/XML レスポンスをまとめて説明:
    • それぞれ別節だったものを「API レスポンス」「非 HTML レスポンス」としてまとめる形。
    • Ajax という古い用語は避け、「API endpoint」「JSON で返す API」などの表現に更新されている可能性が高いです。

2.2.4 レイアウト探索ロジックの整理

  • 「Finding Layouts(レイアウトの見つけ方)」がガイド冒頭でも末尾でも説明されていたため、内容を DRY に再編。
    • レイアウト探索ルールをどこか1カ所に集約し、他の場所からはリンクする形に。
  • ビルダー(例えば Jbuilder など)とレイアウトの関係に関する情報も、重複を減らすように整理。
  • セクション 3 冒頭で「前に説明したレイアウト探索ルール」とだけ書いていた箇所に、明示的なリンクを追加する形に修正。

2.2.5 Request Variants の移動

  • 元々 Action Controller Overview にあった リクエストバリアント(request.variant / respond_to 経由など) の節を Layouts and Rendering ガイドへ移動。
  • これに伴い:
    • Action Controller Overview には「レンダリングのごく短いイントロ」を書き足し、
    • 詳細な内容は Layouts and Rendering ガイドへ誘導する形に。

2.2.6 Action View Overview の拡充

Layouts and Rendering から削った「ビュー側の話」をこちらに統合:

  • 移動/強化されたテーマ:
    • レイアウトの構造 (application.html.erbyieldcontent_for)
    • アセットタグヘルパー (javascript_include_tag, stylesheet_link_tag, image_tag など)
      → ここでまとめて説明し、Layouts and Rendering からは削除
    • パーシャル (render "form", コレクションのレンダリング、ローカル変数の渡し方など)
    • ネストしたレイアウト(使い所が限定的なため、説明を薄くするか・削るかの方向)
  • 「Layouts and Rendering なのに、ビューの話ばかり」という状態を避け、ビューの話は概ねこちらに集約。

2.2.7 Action Controller Advanced Topics, Overview の差分

  • action_controller_advanced_topics.md
    • レンダリング関連の説明が少し削られ、内容を他ガイドへ移動。
  • action_controller_overview.md
    • 行数が +1003 / -839 と大きく変化しており、
      • コントローラの役割
      • レスポンスの作り方(render, redirect_to, head の概要)
      • Layouts and Rendering へのリンク など、**「レンダリングへの入口」としての説明が手厚くなっていると考えられます。

  1. 影響範囲・注意点
  • コードへの直接の影響はなし(ガイドのみの変更)ですが、学習・参照パターンには影響があります。
    • 以前: Layouts and Rendering ガイドに「パーシャル」「アセットタグ」「yield/content_for」「ネストレイアウト」などが分散していた。
    • 今後:
      • コントローラが何を返すか/どうレンダするか → Layouts and Rendering
      • ビュー/レイアウトの書き方・構造・ヘルパー → Action View Overview
  • 古いブログ記事や社内ドキュメントで
    • 「Layouts and Rendering ガイドの ○○セクションを参照」といったリンクや案内をしている場合、対応する場所が Action View Overview に移っている可能性があるので注意。
  • render inline: のように「存在はするが推奨されない API」については、ガイド上も「基本使わない」トーンが強まっているため、
    • これらに依存するコードを書く理由/必要性は、より慎重に検討すべき、というメッセージと読み取れます。
  • JSON/XML/API 向けの説明が整理されているため、
    • 新規開発者には「Ajax 用ビュー」ではなく「API エンドポイントとしてのコントローラ・レスポンス」という観点で教えるのが自然になります。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/57152
  • 関連しそうなガイド(変更後を前提として探すとよいもの):
    • Layouts and Rendering: guides/source/layouts_and_rendering.md
    • Action View Overview: guides/source/action_view_overview.md
    • Action Controller Overview: guides/source/action_controller_overview.md
  • MIME タイプの基礎:
  • レスポンスヘッダだけ返す用途の例:
    • head :no_content(204 No Content): 成功だが本文不要な API
    • head :not_found(404): リソースが存在しない場合に本文不要で返す
      (これらはガイドの head セクションで例示されている可能性があります)

#58628 Use new max_connections instead of pool in guide [ci skip]

マージ日: 2026/9/4 | 作成者: @rnestler

  1. 概要 (1-2文で)
    Rails 8.1 でデータベース設定オプション poolmax_connections に名称変更されたことを受けて、公式ガイド内の記述を新しいオプション名に合わせて修正した PR です。コードや挙動の変更ではなく、ドキュメント(ガイド)のみを対象とした修正です。

  1. 変更内容の詳細

何が変わったか

  • 対象ファイル

    • guides/source/action_cable_overview.md
    • guides/source/configuring.md
  • 主な変更点

    • これまでデータベース接続数を表す設定としてガイド中で使われていた pool というキー名を、Rails 8.1 で正式に導入された max_connections というキー名に置き換えています。
    • pool という用語の説明やサンプル設定が、max_connections の説明・サンプル設定に更新されています。

想定されるガイド内の記述例(イメージ)

※PR本文からは正確な文面までは分かりませんが、Rails のガイド構成からすると、以下のような記述が poolmax_connections に書き換えられていると考えられます。

yaml
# 旧: Rails 8.0 までの例(ガイドに載っていた想定)
production:
  adapter: postgresql
  database: my_app_production
  pool: 5
  timeout: 5000

# 新: Rails 8.1 以降の例(このPRでガイドがこういった形に更新)
production:
  adapter: postgresql
  database: my_app_production
  max_connections: 5
  timeout: 5000

Action Cable のガイド (action_cable_overview.md) についても、内部で利用するデータベース接続数に関連する設定例や説明部分が pool から max_connections に読み替えられています。


  1. 影響範囲・注意点
  • コード・挙動への影響

    • この PR 自体はドキュメントのみの変更であり、Rails 本体の挙動には一切影響しません。
    • ただし、Rails 8.1 を使う開発者がガイドを見たときに、正しい設定キー名(max_connections)が参照できるようになるという意味で、実務上の混乱を防ぐ効果があります。
  • マイグレーションの観点

    • Rails 8.1 からは poolmax_connections にリネームされています。
    • 既存アプリの database.yml を Rails 8.1 に合わせる場合は、ガイドに倣って以下のような変更が必要になる可能性があります(※実際の互換性・非推奨期間などは CHANGELOG / 該当PR を要確認)。
      diff
      - pool: 10
      + max_connections: 10
    • 新たに Rails 8.1 でアプリを作る場合は、最初から max_connections を使うべきです。
  • ドキュメントとの整合性

    • ガイドが pool ではなく max_connections を前提にすることで、
      • Active Record の新しい設定オプション群(keepalive, max_age, min_connections, max_connections)との整合性が取れる
      • 「コネクションプールのサイズ」という概念が、より直感的な名前(最大接続数)で説明される
        といったメリットがあります。

  1. 参考情報 (あれば)
  • Rails 8.1 Active Record CHANGELOG(PR本文で言及)
    • keepalive, max_age, min_connections の追加
    • poolmax_connections へのリネーム
  • 関連 PR
    • #57736: 新オプションに関する追加ドキュメント
    • #54175: max_connections オプションを最初に導入した PR

Rails 8.1 以降で DB コネクション関連を調整する場合は、max_connections / min_connections / keepalive / max_age をセットで確認しておくとよいです。


#58665 [8-1-stable] Fixed guard around AS::TC.run_order

マージ日: 2026/9/4 | 作成者: @fesiqueira

  1. 概要 (1-2文で)
    Rails 8.1系(8-1-stable ブランチ)で、ActiveSupport::TestCase.run_order を扱うガード条件(安全な存在チェック)が誤っていたのを 1行だけ修正したバックポートPRです。Minitest 6 対応のフォローアップで、テスト実行順序の取得まわりで発生しうる不具合を防ぐ目的の修正です。

  1. 変更内容の詳細

※このPRは activesupport/lib/active_support/test_case.rb の 1 行差し替えのみです。
元PR (#56879) および Minitest 6 対応 (#56434) のフォローアップで、「AS::TC.run_order を参照する際の条件分岐(ガード)」が不適切だったのを修正しています。

典型的には、次のような処理が関係します(擬似コード):

ruby
module ActiveSupport
  class TestCase < Minitest::Test
    # テストの実行順序を Rails 側から制御/参照するコード
    def self.test_order
      # ここで Minitest::Test.run_order や ActiveSupport::TestCase.run_order を見たり、
      # Minitest のバージョンによって存在しない場合があるメソッド/定数を
      # 条件付きで呼び分ける
    end
  end
end

Minitest 6 では API や挙動が微妙に変わっており、例えば:

  • run_order が定義されていない状況で無条件に呼び出すとエラーになる
  • nil や想定外の値を返す可能性を考慮していない条件式だと、テスト順序の設定が正しく反映されない

といった問題が起こりえます。

このPRでは、その「run_order を利用する前のチェック条件」だけを 1行修正し、

  • run_order が存在しない/使えないケースでも例外にならない
  • Rails 側で意図した実行順序(例: :random, :sorted など)が正しく反映される

ようにしています。

実際の差分イメージ(あくまで例示):

diff
- if AS::TC.respond_to?(:run_order)
+ if AS::TC.respond_to?(:run_order) && AS::TC.run_order
    # run_order が有効なときだけ特定の処理を行う
  end

あるいは、Minitest 5 / 6 でのインターフェイス差異を正しく判定するようなガードに修正されています。


  1. 影響範囲・注意点
  • 影響範囲

    • Rails 8.1 系で Minitest を使っているプロジェクトの「テスト実行順序(test order)」周りに影響します。
    • 特に Minitest 6 系と組み合わせたときに、
      • テストスイートの実行開始時に NoMethodErrorundefined method 'run_order' のようなエラーが出ていたケース
      • config.active_support.test_orderENV['MT_RUN_ORDER'] などと組み合わせた挙動が不安定だったケース
        が安定することが期待されます。
  • 互換性 / 破壊的変更について

    • 1 行のガード修正のみであり、run_order が正しく提供されている環境ではほぼ影響はありません。
    • 既存の Minitest 5 系を利用している Rails 8.1 プロジェクトでも、仕様上の挙動(ランダム実行やソート順など)は基本的に変わらない想定です。
    • バグ回避のための条件式強化が中心で、「仕様変更」というより「正しい条件に合わせた」修正に近いです。
  • 注意点

    • 8.1 系を Minitest 6 と組み合わせている場合は、この修正が含まれるパッチレベルにアップデートしておくと安全です。
    • Rails 本体側のテスト(特に ActiveSupport::TestCase を継承した独自テストクラス)で、run_order を直接参照しているような高度なカスタマイズを行っている場合は、一度テストを走らせて想定通りの順序になっているか確認してください。

  1. 参考情報 (あれば)
  • 元PR(本流ブランチ向け):
    • #56879 — Minitest 6 対応の follow-up として AS::TC.run_order 周りを修正した PR
  • 元の Minitest 6 対応 PR:
    • #56434 — Rails が Minitest 6 で動作するようにするための対応一式
  • このPR:
    • #58665 は上記 #56879 のバックポート (git cherry-pick -x e2feab4...) を 8-1-stable に適用したものです。

#58666 Use equality comparison when fetching information about a single table

マージ日: 2026/9/4 | 作成者: @ilianah

  1. 概要 (1-2文で)
    MySQL 用スキーマリーダが「単一テーブル」に対しても IN (...) を使って information_schema を問い合わせていた挙動を、1件の場合は = 比較に戻すPRです。Vitess 環境で remove_index などが正しく動かずエラーになっていた問題を、古い Vitess バージョンも含めてワークアラウンドする目的があります。

  1. 変更内容の詳細

背景

  • #58421 以降、MySQL のスキーマリーダは「テーブル情報を取るメソッド」が常に「テーブル名のリスト」を前提とする実装になり、SQL も table_name IN (...) 形式で発行されるようになりました。
  • Vitess 環境では 1つの「論理DB」に複数 keyspace がぶら下がる構成があり、information_schema が keyspace ごとに分かれているため「どの keyspace の information_schema に投げるべきか」をルーターが解決できません。
  • 特に IN 句で information_schema を叩くクエリは keyspace 解決に失敗し、「ルーティングできない」→「デフォルト keyspace に投げる」挙動になり、デフォルト keyspace 外のテーブルについては空結果が返ってしまいます。
  • Rails の remove_index は内部で fetch_indexes(table_name) を呼び出し、これが IN クエリで index 情報を取りに行くため、Vitess 上では「インデックスが存在しても見つからない」という状態になり、
    ruby
    No indexes found on #{table_name} with the options provided.
    というエラーを引き起こしていました。

Vitess 本体側でも修正 (vitessio/vitess#20973) が入りますが、v23 以降にしか入らないため、それより古いバージョンを使うユーザには影響が残ります。

このPRでの修正方針

  • 単一テーブル の情報を取得する場合」は IN ではなく =(等価比較)に戻す。
  • 複数テーブル」を対象とするクエリは従来どおり IN (...) のまま据え置き(Vitess では依然として問題になるが、少なくとも remove_index など単一テーブル向けの主要ユースケースを救済できる)。

コード上の変更イメージ

※実際の行差分は抜粋レベルですが、概念的には次のような変化です。

変更前(単一テーブルでも IN

ruby
def quoted_scope(table_names)
  # table_names は単一/複数問わず配列前提
  "AND table_name IN (#{table_names.map { |t| quote(t) }.join(', ')})"
end

変更後(1件なら =、複数なら IN

ruby
def quoted_scope(table_names)
  names = Array(table_names)

  if names.size == 1
    # 単一テーブル → 等価比較
    "AND table_name = #{quote(names.first)}"
  else
    # 複数テーブル → 従来通り IN
    "AND table_name IN (#{names.map { |t| quote(t) }.join(', ')})"
  end
end

これに付随して、abstract_mysql_adapter 側のヘルパーメソッドも「常に list 前提」だったものを、「1件の場合の処理分岐」を持つ形に調整しています(+6/-10 程度の軽微なリファクタリング)。


  1. 影響範囲・注意点

影響範囲

  • 対象:
    • MySQL 系アダプタ(mysql2 など)を使った Active Record
    • スキーマ情報を information_schema から取得する処理全般
      • remove_index / index_exists? / マイグレーション実行時の index 情報参照 など
  • 特に恩恵を受けるケース:
    • Vitess 上で Rails アプリを動かしており、かつ
    • 複数 keyspace を使っている or テーブルがデフォルト keyspace 以外にある

この PR 適用後は、単一テーブルに対して index 情報を取るクエリが等価比較に戻るため、Vitess の keyspace ルーティングが正しく働き、remove_index などが期待通り動作するようになります。

注意点

  • 複数テーブルを対象とするクエリは依然として Vitess では問題になりうる
    • IN を使うクエリは Vitess が keyspace を解決できず、デフォルト keyspace で実行される挙動は変わりません。
    • ただし、この PR の目的は「単一テーブル向けの主要ユースケース(remove_index など)を壊さないこと」であり、複数テーブル同時問い合わせの挙動は元々も保証されていなかったため据え置きになっています。
  • Vitess v23 以降を使う場合
    • Vitess 側に IN 句サポートの修正が入るため、理論上は Rails 側のワークアラウンドがなくても問題は軽減されますが、この PR による挙動変更は後方互換的であり、v23 を使っても悪影響はほぼありません(単一テーブルクエリが = になるだけ)。
  • MySQL 単体利用・非 Vitess 環境
    • ローカル MySQL や一般的なクラウド MySQL では、IN ('users')= 'users' は実質同じ意味なので、パフォーマンスや動作への影響はほぼありません。

  1. 参考情報 (あれば)
  • 元PR:
    • rails/rails#58666: Use equality comparison when fetching information about a single table
  • 関連PR:
    • rails/rails#58421: MySQL schema readers answer for a list of tables(今回の挙動変更の発端となったPR)
    • vitessio/vitess#20973: Vitess 側で information_schema + IN 句に関するルーティング問題を修正するPR
  • 関連するRails機能:
    • ActiveRecord::ConnectionAdapters::Mysql::SchemaStatements
    • remove_index, index_exists?, マイグレーションでの index 操作全般

#58623 Check PATCH and QUERY in routing assertions with method: :all

マージ日: 2026/9/3 | 作成者: @carlosdanielpohlod

  1. 概要 (1-2文で)
    Rails のルーティングテスト用アサーション assert_recognizes / assert_routing において、method: :all 指定時に PATCH と新たに追加された QUERY メソッドも検証対象とするように修正した PR です。これにより、「全ての HTTP メソッドを受け付けるはず」という前提で書かれたテストが、実際には PATCHQUERY を受け付けていないルートを正しく検出できるようになります。

  1. 変更内容の詳細

2-1. 背景と問題点

  • Rails のテスト用アサーション:

    • assert_recognizes(expected_options, path, method: ...)
    • assert_routing(path, expected_options, method: ...)
  • これらは method: :all を指定すると、「そのパスが全 HTTP メソッドでルーティング解決される」ことを検証する意図のオプションになっている。

  • しかし、実装上は GET, POST, PUT, DELETE の 4 つしかチェックしていなかった

  • そのため、以下のようなルート定義をしていても:

    ruby
    # PATCH と QUERY を受け付けない
    match "/posts/:id", to: "posts#update", via: [:get, :post, :put, :delete]

    テスト側で:

    ruby
    assert_routing "/posts/1", { controller: "posts", action: "update", id: "1" }, method: :all

    と書くと、「全ての HTTP メソッドにマッチする」と主張しているにもかかわらず、実際には PATCHQUERY は通っていない状態でテストが通ってしまう、という不整合があった。

  • このリスト (GET, POST, PUT, DELETE) は Rails 4 以前に由来しており、

    • 当時は PATCH が更新のデフォルトメソッドになる前だった
    • QUERY は今回の開発サイクルで追加された HTTP メソッド という歴史的経緯がある。

2-2. 今回の修正内容

(1) method: :all がチェックするメソッドを拡張

action_dispatch/testing/assertions/routing.rb 内の実装を変更し、method: :all のときにチェックされる HTTP メソッドに :patch:query を追加しました。

※ 実際のコード断片(イメージ):

ruby
# 変更前(イメージ)
HTTP_METHODS_FOR_ALL = [:get, :post, :put, :delete].freeze

# 変更後(イメージ)
HTTP_METHODS_FOR_ALL = [:get, :post, :put, :delete, :patch, :query].freeze

これにより、以下のようなテストが:

ruby
assert_routing "/articles/1",
  { controller: "articles", action: "show", id: "1" },
  method: :all

内部的には少なくとも:

ruby
[:get, :post, :put, :delete, :patch, :query].each do |http_method|
  assert_routing "/articles/1",
    { controller: "articles", action: "show", id: "1" },
    method: http_method
end

に相当するチェックを行うようになります。

(2) OPTIONS, HEAD, TRACE を含めなかった理由の明示

PR 内で、以下のメソッドは method: :all の検証対象から外した理由が説明されています:

  • HEAD:
    • Rails では一般に GET ルートから自動的に提供されるため、個別チェックは不要と見なした。
  • OPTIONS:
    • 多くの場合 Rack ミドルウェア(CORS ハンドラなど)が応答し、アプリ側のルーティングで扱わないケースが多い。
  • TRACE:
    • 実務ではほとんどルーティングしないため対象外とした。

ただし、必要であればこれらを含める変更も検討可能である、というスタンスが示されています。

(3) ドキュメントとテストの追加

  • method: :all オプションに関する RDoc が存在しなかったため、ドキュメントを追加。
  • actionpack/test/controller/resources_test.rb に、PATCH / QUERY を含めた形での method: :all の挙動を検証するテストを追加。
    • 既存のテスト名 test_assert_routing_fails_when_not_all_http_methods_are_recognized の挙動が、PATCH / QUERY も含めて「全ての対象メソッド」をチェックするように強化されている。

(4) CHANGELOG の更新

  • actionpack/CHANGELOG.md に、本変更によって挙動が厳密になり、既存のテストが失敗する可能性があることが明記されています。

  1. 影響範囲・注意点

3-1. 既存テストへの影響

最も重要な影響点は、「今まで通っていたテストが落ちる可能性がある」ことです。

条件としては:

  • テストコード:
    • assert_recognizes または assert_routingmethod: :all を指定している
  • ルーティング定義:
    • 対象のルートが PATCH または QUERY を受け付けていない (via: [:get, :post, :put, :delete] のような定義 など)

この場合、今回の修正後は PATCH / QUERY でもそのパスが解決されることを期待されるため、テストが失敗します。

これは PR の意図通りのブレイキング変更であり、「本当にすべての HTTP メソッドを受け付けるべきルートか?」を見直すきっかけになります。

3-2. 対応パターン

テストが落ちた場合の代表的な対応は次の 3 つです。

  1. ルート定義を更新して本当に “all” を受け付けるようにする

    ruby
    # 以前
    match "/posts/:id", to: "posts#update", via: [:get, :post, :put, :delete]
    
    # 修正後: PATCH / QUERY を含める
    match "/posts/:id", to: "posts#update", via: :all
    # または
    match "/posts/:id", to: "posts#update", via: [:get, :post, :put, :delete, :patch, :query]

    そして method: :all のテストを維持する。

  2. テストが “全メソッド” である必要はなかった場合

    もともと「特定のメソッドだけ通ればよい」ルートであるのに、便宜上 method: :all を使っていた場合は、明示的にメソッドを指定する:

    ruby
    assert_routing "/posts/1",
      { controller: "posts", action: "update", id: "1" },
      method: :patch  # あるいは :get, :post など必要なものだけ

    もしくは、[:get, :post] のように自前でループを書くなど。

  3. QUERY をサポートしない方針の場合の検討

    • Rails 側に QUERY が追加されたこと自体を念頭に置き、自アプリで QUERY をサポートするかどうか設計から見直す必要がある場合があります。
    • 現時点で QUERY を完全に使わないポリシーなら、method: :all は使わず、必要なメソッドに限定したテスト戦略を取る方が明確です。

3-3. 新規開発時のベストプラクティスへの示唆

  • via: :all でルートを定義し、テスト側でも method: :all を使う」ことがより正しく噛み合うようになりました。
  • PATCH が更新系のデフォルトであること、QUERY メソッドが Rails に追加されていることを前提に、本当に全メソッドを許可したいケース以外では via: / method: の指定を明示的かつ限定的にするのが安全です。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58623
  • 関連コード:
    • actionpack/lib/action_dispatch/testing/assertions/routing.rb
    • actionpack/test/controller/resources_test.rb
    • actionpack/CHANGELOG.md
  • assert_routing / assert_recognizes ドキュメント (Rails Guides / RDoc) も今回の変更で method: :all が明文化されているため、最新版の公式ドキュメント参照が推奨されます。

#58651 SchemaContext#attributes: preload schemas to prevent deadlock

マージ日: 2026/9/3 | 作成者: @kyrofa

  1. 概要 (1-2文で)
    SchemaContext#attributesRactor.store_if_absent によるロックを使って Attributes を初期化していた結果、スキーマ読み込み時に再帰的に同じロックを取りに行ってデッドロックが発生しうる問題を解消した PR です。
    ロックを完全に取り除き、デッドロックを防ぎつつ動作をテストで担保しています。

  1. 変更内容の詳細

問題の構造

SchemaContext#attributes は「そのクラスの Attributes オブジェクト」を遅延初期化する箇所です。元の実装イメージは概ね次のような流れになっていました:

  1. SchemaContext#attributes が呼ばれる
  2. Ractor.store_if_absent を使って、Attributes をスレッド/Ractor 安全にキャッシュ
  3. その際 Attributes.new が呼ばれる
  4. Attributes.new 内で AttributeRegistration#apply_pending_attribute_modifications を実行
  5. ここでスーパークラス側の attribute 修飾(例: EncryptableRecord の暗号化関連)を適用するため、クラス階層を遡りながら columns_hash を呼ぶ
  6. columns_hash がスキーマのロードをトリガし、load_schema → 再度 SchemaContext#attributes を呼ぶ
  7. 2 に戻り、同じ Ractor.store_if_absent でロックを取りに行く → 再入ロック不可なためデッドロック

つまり「Attributes 初期化 → attribute 修飾の適用 → スキーマロード → 再度 Attributes 初期化」のループの途中で、同じロックを再取得しようとして固まる、という構造的な問題です。

この PR の修正

修正の核心は「SchemaContext#attributes 内で使っていた Ractor.store_if_absent によるロックを削除する」ことです。

変更点は以下の通りです:

  • activerecord/lib/active_record/model_schema/schema_context.rb
    • SchemaContext#attributes から Ractor.store_if_absent 呼び出しを削除
    • 代わりに、ロックなしで Attributes を生成・利用する形に変更
    • 行レベルでは +1/-3 なので、ほぼ「ロックまわりの削除」のみ

擬似コードでの Before / After は以下のようなイメージです(実際のコードは多少異なりますが概念的にはこうです):

ruby
# Before (イメージ)
def attributes
  Ractor.store_if_absent(@attributes_key) do
    Attributes.new(self)
  end
end

# After (イメージ)
def attributes
  # ロックなしに Attributes を返す/生成する
  @attributes ||= Attributes.new(self)
end

実際には Ractor 対応用のストアを使っているためこのままではない可能性がありますが、ポイントは「Ractor.store_if_absent ベースのロックメカニズムをやめた」ことです。

テストの追加

  • activerecord/test/cases/attributes_test.rb に 17 行追加
    • 再帰的に SchemaContext#attributes → スキーマロード → SchemaContext#attributes が呼ばれるケースでもハングしないこと
    • EncryptableRecord やスーパークラス側の attribute 修飾を含むような実運用に近いシナリオをカバーしていると考えられます
    • デッドロックが再発しないことを保証する回帰テストとして機能

  1. 影響範囲・注意点

影響しうる領域

  • Active Record のスキーマロード周り (load_schema, columns_hash)
  • モデルの attributes 定義とその修飾(AttributeRegistration, 暗号化属性を提供する EncryptableRecord など)
  • Ractor / 並列実行環境での Attribute キャッシュ

これらを使うすべてのアプリケーションが潜在的な影響範囲です。

デッドロック解消の代償としての懸念点

  • ロック削除により、「同時に複数の Ractor/スレッドが同じクラスの Attributes を初期化しに行く」ケースで
    • 同じ Attributes に対して重複初期化が発生する可能性
    • ただし Active Record の attribute 定義は基本的に idempotent かつスキーマ/メタデータであり、多少の重複計算は許容される設計になっていることが多い
  • 本 PR は「安全性よりも、致命的なデッドロックを確実に回避する」ことを優先した修正といえる

実運用での注意点

  • Rails アプリで以下のような条件がそろうときに、この修正の恩恵を強く受けます:
    • モデルに暗号化属性などの attribute 修飾(EncryptableRecord やそれに準ずるモジュール)を利用している
    • 並列実行/Ractor を利用している、もしくは並列なスキーマロードが起こりうる
    • 起動時や初回アクセス時にランダムにハングするような現象があった場合、このバグに該当していた可能性がある
  • PR #58650 に紐づくバグであり、その Issue に類似する症状(スキーマロード中に固まる)が起きていた場合、Rails バージョンをこの修正を含むものまで上げる価値があります。

  1. 参考情報 (あれば)
  • この PR が修正する Issue: #58650
    (SchemaContext#attributes / Ractor.store_if_absent に起因するデッドロック報告)
  • 関連クラス・メソッド:
    • ActiveRecord::ModelSchema::SchemaContext#attributes
    • ActiveRecord::AttributeRegistration#apply_pending_attribute_modifications
    • ActiveRecord::Encryption::EncryptableRecord(暗号化属性を提供するモジュール)
    • ActiveRecord::Base#columns_hash, ActiveRecord::ModelSchema#load_schema

#58656 Prevent freezing Action Dispatch default headers config itself

マージ日: 2026/9/3 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Rails の ActionDispatch が持つ「デフォルトレスポンスヘッダ設定」を Ractor 安全性のために freeze する際、Rails.application.config.action_dispatch.default_headers そのものが凍結されてしまう問題を修正し、設定オブジェクト自体は凍結されないようにした PR です。これにより、古くから存在する「load hook 内で config を破壊的変更する」ようなコードがクラッシュせず動作(※実質的な効果は元々ほぼなかった)を続けられるようになります。

  1. 変更内容の詳細

背景

  • 以前のコミット(56d19e5...)で、Ractor 対応の一環として「Action Dispatch のデフォルトヘッダ」を freeze するようになった。

  • その際に「ヘッダの中身」だけでなく、「Rails.application.config.action_dispatch.default_headers(設定オブジェクト)そのもの」も結果的に freeze されてしまっていた。

  • そうすると、以下のように load hook の中で default_headers を破壊的変更しようとするコードがクラッシュするケースが出る:

    ruby
    initializer :mutation do
      ActiveSupport.on_load(:action_controller) do
        Rails.application.config.action_dispatch.default_headers.clear
      end
    end
    • ActionController::Base が先にロードされると ActionDispatch::Response もロードされ、そのタイミングで default_headers が freeze される。
    • その後に ActionController::API をロードすると load hook がもう一度呼ばれ、2 回目の clear が「frozen なオブジェクトへの変更」となり例外が発生する。

このようなコードは設計的には「やるべきではない」ものの、10 年ほど問題なく動いていたため、いきなり壊すのは避けたいという判断になっています。

具体的な修正

変更ファイルは 2 つだけです。

  1. actionpack/lib/action_dispatch/railtie.rb (+1 / -1)

    ここで行っていることは「freeze 対象を変える」ことです。

    おおまかには次のようなイメージになります(実際のコードはこれと等価なことをより Rails 的な書き方でやっていると考えてください):

    ruby
    # 以前のイメージ(問題があった実装)
    config.action_dispatch.default_headers.freeze
    
    # 今回の修正イメージ
    headers = config.action_dispatch.default_headers.dup   # 設定値をコピー
    headers.freeze                                         # コピー側を freeze
    ActionDispatch::Response.default_headers = headers     # Response 側に渡す

    重要な点は:

    • Rails.application.config.action_dispatch.default_headers凍結しない
    • ActionDispatch::Response が使う実体としては「コピーされたヘッダ Hash(もしくは類似オブジェクト)」を freeze して利用する。
    • したがって、アプリ・ライブラリ側から Rails.application.config.action_dispatch.default_headers をその後に変更しても、実際のレスポンスヘッダには反映されない(これは元々もほぼ同様の挙動)。
  2. railties/test/application/configuration_test.rb (+11 / 0)

    テストが追加されています。主に次のようなことを確認しています:

    • ActionDispatch 読み込み後でも、Rails.application.config.action_dispatch.default_headers が freeze されていないこと。
    • つまり、config.action_dispatch.default_headers.clear や他の破壊的操作を行っても「FrozenError」等を投げずに動くこと。

  1. 影響範囲・注意点

影響範囲

  • 影響を受けるのは主に以下のようなコードを持つアプリ/ライブラリです:

    • ActiveSupport.on_load(:action_controller) や他の load hook 内で Rails.application.config.action_dispatch.default_headers を破壊的に変更しているコード。
    • 特に「ActionController::BaseActionController::API 両方がロードされる環境」で、同じ load hook が複数回呼ばれる場合。
  • この PR 適用後:

    • これらのコードは 例外なく動作を継続 します。
    • ただし、ActionDispatch::Response が実際に使うデフォルトヘッダは freeze 済みのコピーなので、
      • 「Response 実行中のヘッダ挙動」がこれらの後付け変更で書き換わることはなく、
      • 設定オブジェクトを書き換えても「見かけ上書き換えられたように見えるが、実際のレスポンスに影響しない」状態になります(これは仕様として以前からほぼ同じで、新たな挙動ではない)。

注意点・ベストプラクティス

  • PR の説明にもある通り、load hook 内で Rails.application.config を破壊的変更するのは推奨されません

    • 設定は通常、環境ごとの config ファイル(config/environments/*.rb など)で「起動時に一度だけ」行うのが筋です。
    • 特に Ractor 対応以降は、マルチスレッド/マルチ Ractor 環境でも予測可能な挙動を維持するため、設定値は immutability を前提として扱う傾向が強まっています。
  • もし「ActionDispatch::Response が使うデフォルトヘッダをアプリケーション側から確実に制御したい」場合は、

    • 起動時に config.action_dispatch.default_headers = { ... } のように設定し、
    • それ以降は変更しない(変更しても意味がない)という前提で設計した方が安全です。

  1. 参考情報 (あれば)
  • 元になった Ractor 対応のコミット: 56d19e536b409b17d8f89545ff5281c4e1c3eaf6

    • ActionDispatch のデフォルトヘッダを freeze して Ractor セーフにする修正。
  • 今回の PR での方針:

    • Ractor 安全性(レスポンスヘッダの immutability)は維持。
    • 10 年以上動いていた既存コードとの後方互換性も尊重し、config.action_dispatch.default_headers 自体は凍結しないようにする。
    • 将来的には、こうした「load hook 中で config を mutate する」パターンはライブラリ側で修正していくべき、という含みを持った暫定的な互換性維持策と言えます。

#58635 Re-enable PostgreSQL triggers when disable_referential_integrity raises

マージ日: 2026/9/3 | 作成者: @lukasedw

  1. 概要 (1-2文で)
    PostgreSQL 18.4 未満の環境で disable_referential_integrity 実行中に例外が発生した場合でも、必ずトリガーが再有効化されるように修正した PR です。これにより、テストやクリーナー処理の失敗をきっかけに、DB 全体の外部キー制約が恒久的に無効化されてしまうバグを防ぎます。

  1. 変更内容の詳細

問題の背景

  • PostgreSQL 18.4 より前のバージョンには、外部キーを一時的に「NOT ENFORCED」にする機能がないため、Rails の disable_referential_integrity は内部的に

    sql
    ALTER TABLE ... DISABLE TRIGGER ALL;
    -- ブロック実行
    ALTER TABLE ... ENABLE TRIGGER ALL;

    というカタログ(テーブル定義)レベルの変更を行います。

  • 元の実装では ENABLE TRIGGER ALLensure で保護されていなかったため、ブロック内で例外が発生すると「トリガーを有効に戻す処理」が実行されず、

    • 外部キー用トリガー(FK トリガー)が全て無効のまま残る
    • これはセッションではなくカタログ状態なので、同一 DB を使う全てのコネクション・プロセスに影響が波及
      する、かなりクリティカルな状態になっていました。
  • #50196 で fixtures のケースについては、「トランザクション内で DISABLE TRIGGER を行う」ことで、例外時に ROLLBACK によって元に戻るようになりましたが、

    • トランザクション外で disable_referential_integrity を呼ぶケース(truncate_tables, DatabaseCleaner の truncation strategy など)は依然として危険なままでした。
  • 実際の問題例:

    • parallel_tests 実行中、あるワーカーで truncate によるデッドロックが発生して例外で落ちる
    • その時 ENABLE TRIGGER ALL が実行されず、以降その DB では:
      • ON DELETE CASCADE が動かない
      • RESTRICT がエラーを出さない
      • SET NULL が実行されない
    • 後続テストが外部キー前提で書かれていると、ランダムにテストが落ちる(後の別 truncate がたまたま成功して元に戻ることもあるため、非常に「フレーク」っぽく見える)

この PR の修正内容

1) ENABLE TRIGGER ALLensure ブロックに移動

  • activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rbdisable_referential_integrity(PostgreSQL 用実装)で、トリガー再有効化処理を ensure に移動しました。

概念的には以下のようなイメージです(簡略化した Ruby 擬似コード):

ruby
def disable_referential_integrity
  disable_triggers_for_all_tables

  begin
    yield
  ensure
    # ブロック内で例外が起きても必ず実行される
    begin
      enable_triggers_for_all_tables
    rescue ActiveRecord::ActiveRecordError
      # 既存の挙動を維持:
      # 外側でトランザクションロールバックが行われるケースなどでは
      # ここでの失敗は握りつぶす
    end
  end
end
  • 重要な点:
    • 既存の rescue ActiveRecord::ActiveRecordError は維持されています。
    • これは「外側にトランザクションがあり、内側で DISABLE した後、外側のロールバックで状態が戻る」ケースで、
      トリガー再有効化の ENABLE が失敗しても、元のトランザクションロールバックで状態が正しく戻る、という挙動を壊さないためです。
      → これについては test_does_not_break_transactions でカバーされています。

2) PostgreSQL 18.4+ (NOT ENFORCED パス) は変更なし

  • #57378 で導入された NOT ENFORCED を使うコードパス(PostgreSQL 18.4+)は、「単一トランザクション内で ON/OFF を切り替え、最後にトランザクションでまとめて戻す」実装になっており、そもそも今回の問題が発生しません。
  • そのため、18.4+ のコードパスには変更を加えていません。

3) 回帰テストの追加

  • activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb に新しいテストを追加:

    • ブロック内で例外を発生させる:

      ruby
      connection.disable_referential_integrity do
        raise "boom"
      end
    • rescue した後、子テーブルに「無効化されたトリガー」が残っていないことを確認するテストです。

      • pg_trigger テーブルを参照し、tgenabled = 'D'(Disabled)なトリガーが残っていないかをチェックする形。
  • テストは以下の条件で skip されます:

    • PostgreSQL 18.4 以上の場合
      → 実装パスが異なり、このバグ自体が起こらないため。
    • テスト実行ユーザが superuser でない場合
      DISABLE TRIGGER ALL は superuser でないと実行できず、その場合 no-op になってしまうため。
      no-op だとテストが「たまたま」通ってしまい、回帰検知にならないため。

4) CHANGELOG の更新

  • activerecord/CHANGELOG.md に、本修正に関する記述が追加されています(バグ修正であること、PostgreSQL の referential integrity に関する変更であることなど)。

  1. 影響範囲・注意点

影響範囲

  • 主に影響を受けるのは、PostgreSQL 18.4 未満かつ Rails の disable_referential_integrity を利用しているケースです。

    • truncate_tables を使うテストヘルパ
    • DatabaseCleaner の truncation strategy
    • その他、手動で disable_referential_integrity を呼び出しているコード
  • この修正により、これらの処理中に例外が発生しても、必ずトリガーが元に戻されるため、

    • 外部キー制約が意図せず無効化されたまま放置される
    • それによって、後続テストや本番に近い検証環境で不整合なデータが生まれる
      といった問題が防止されます。

互換性・挙動の変化

  • 期待される(望ましい)意味での挙動の変化:
    • 以前は disable_referential_integrity ブロック内で例外が出ると「トリガーが無効化されたまま」だったが、
      今後は「例外が出てもトリガーは必ず再有効化される」ようになります。
  • 既存のトランザクションとの相互作用は維持:
    • 外側でトランザクションを張った状態で disable_referential_integrity を使うケースでは、従来通り「ロールバックで元に戻る」パターンもサポートされ、テストで確認済みです。

注意点

  • PostgreSQL 側の権限:

    • このコードパスは DISABLE TRIGGER ALL / ENABLE TRIGGER ALL を実際に発行するので、本番環境で使う場合は DB ユーザ権限(superuser か否か)に注意が必要です。
    • 非 superuser では DISABLE TRIGGER ALL は no-op になるので、この修正も実質的には影響しません。
  • PostgreSQL 18.4+ を使っている場合:

    • NOT ENFORCED ベースの別実装が使われるため、この PR の修正は直接の影響はありません。
    • ただし、将来バージョンアップや DB バージョン切り替え時に、「どのパスが使われているか」を意識しておくとデバッグしやすくなります。

  1. 参考情報 (あれば)
  • 元 PR: #58635
  • 関連 PR:
    • #50196 — fixtures 利用時に disable_referential_integrity をトランザクション内で実行するようにした対応
    • #57378 — PostgreSQL 18.4+ の NOT ENFORCED を使った referential integrity の新実装
  • 関連する PostgreSQL の仕様:
    • ALTER TABLE ... DISABLE TRIGGER ALL / ENABLE TRIGGER ALLセッション状態ではなくカタログ状態を変更する点に注意(トランザクションに包まないと永続的な変更になる)。

#58638 Add PostgreSQL adapter support for configuring error_verbosity

マージ日: 2026/9/3 | 作成者: @beauraF

  1. 概要 (1-2文で)
    PostgreSQL アダプタに、接続設定 (database.yml) から error_verbosity を指定して libpq のエラーメッセージ詳細度を制御できる機能が追加されました。これにより、ユニーク制約違反などのエラーに含まれる PII(メールアドレス等)がログ経由で漏洩することを抑制しやすくなります。

  1. 変更内容の詳細

2-1. 何ができるようになったか

config/database.yml の各環境ごとの設定に、error_verbosity キーを追加して、PostgreSQL 接続のエラーメッセージ詳細度を指定できます。指定された値は、接続確立後に PostgreSQLAdapter#configure_connection 内で一度だけ PG::Connection#set_error_verbosity に渡されます。

サンプル:

yaml
production:
  adapter: postgresql
  database: my_app_production
  username: my_user
  password: <%= ENV["DB_PASSWORD"] %>
  error_verbosity: <%= PG::PQERRORS_TERSE %>

Rails 側は、この error_verbosity の値をそのまま pg gem の PG::Connection#set_error_verbosity に渡します。
そのため、利用可能な値は pg / libpq がサポートしている定数になります:

  • PG::PQERRORS_TERSE
    → もっとも簡潔なメッセージ。DETAIL や HINT などが削られ、PII が含まれる可能性を減らしやすい。
  • PG::PQERRORS_DEFAULT
    → PostgreSQL のデフォルト。従来と同等の挙動。
  • PG::PQERRORS_VERBOSE
    → より詳細な情報(内部情報やバックトレースに近いもの)を含む。
  • PG::PQERRORS_SQLSTATE
    → SQLSTATE コード中心の出力。

2-2. 実装のポイント

変更点は主に以下です。

  • activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

    • configure_connection メソッド内で、config[:error_verbosity] が存在する場合のみ @connection.set_error_verbosity(config[:error_verbosity]) を実行するロジックが追加。
    • 既存の encoding 設定と同じパターンで、「設定されていれば一度だけ適用」という方針。
  • activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb

    • error_verbosity が設定されたときに PG::Connection#set_error_verbosity が呼び出されることを検証するテストが追加。
  • activerecord/CHANGELOG.md

    • Active Record への新機能として、「PostgreSQL アダプタで error_verbosity を設定できる」ことが追記。

コードイメージ(Rails 内部の処理イメージ・擬似コード):

ruby
def configure_connection
  super

  if config[:error_verbosity]
    @connection.set_error_verbosity(config[:error_verbosity])
  end
end

  1. 影響範囲・注意点
  • デフォルト動作は変わらない

    • error_verbosity を指定しなければ、これまで通り pg / PostgreSQL のデフォルト設定(PQERRORS_DEFAULT)が利用されます。
    • 既存アプリは、この設定を追加しない限り挙動は変わりません。
  • PII流出対策としての有効性

    • ユニーク制約違反、外部キー制約違反、デッドロックなどのエラーの DETAIL フィールドには、実際に違反した値(例: メールアドレス、電話番号)が含まれます。
    • ruby-pg はこのテキストをそのまま例外メッセージに含めるため、ログやエラートラッカー(Sentry など)に PII が載りがちでした。
    • error_verbosityPG::PQERRORS_TERSE 等に下げることで、この詳細テキストが抑制されるため、ログの情報量とプライバシー保護のバランスを取りやすくなります。
  • 設定値は Ruby コードとして評価されることに注意

    • ドキュメント例でも <%= PG::PQERRORS_TERSE %> のように ERB で PG の定数を参照しています。
    • 誤って文字列 "PG::PQERRORS_TERSE" のように書くと正しく動作しないので、「Ruby の定数として評価させる」記法にする必要があります。
  • 再接続・コネクションプールとの関係

    • configure_connection は接続確立時に呼ばれるので、プールされた各コネクションごとに一度だけ適用されます。
    • 接続切断→再接続が起きても、そのタイミングで再びこの設定が適用されます。
  • 依存バージョン

    • PG::Connection#set_error_verbositypg gem / libpq が対応している必要があります。
    • 通常のサポート対象バージョンの Rails + pg であれば問題ないはずですが、極端に古い pg を使っている場合は事前に確認が必要です。

  1. 参考情報 (あれば)

#58647 Make the controller configs shareable in ractorize!

マージ日: 2026/9/2 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Railsアプリを ractorize! した際に、コントローラのコンフィグ(設定オブジェクト)が Ractor 間で安全に共有できるようにした PR です。アプリケーションブート後の「コントローラレベルの設定」は共有可能な形に固定し、各コントローラインスタンス側で変更可能なコピーを持つ設計に寄せています。

  1. 変更内容の詳細

※PR本文と差分構成からの推測を含みますが、Rails の既存構造と整合するように整理しています。

2-1. 背景: Ractor と controller configs

  • Ruby 3 以降の Ractor では「shareable(共有可能)」なオブジェクトしか Ractor 間で共有できません。
  • Rails には ActionController::Base に紐づくクラスレベルの設定(例: config.action_controller.perform_caching のようなコントローラ設定オブジェクト)があり、その多くは ActiveSupport::OrderedOptions を使って保持されています。
  • これら設定オブジェクトが「共有不可能(unshareable)」な状態だと、Rails.application.ractorize! 実行時にエラーや予期しない挙動の原因になります。

PRでは、この「コントローラの設定オブジェクト」を Ractor 的に安全な状態(shareable)にする対応が入っています。


2-2. Rails.application.ractorize! 内で controller configs を shareable に

railties/lib/rails/application.rb にある Rails.application.ractorize! 処理の中で、コントローラ設定を shareable にする処理が追加されています(+6行)。

典型的には、以下のような処理が入っているイメージです(あくまで擬似例):

ruby
def ractorize!
  # 既存: 他の設定やロードのための処理...

  # 追加: controller configs を shareable にする
  # 例: ApplicationController やそのサブクラスの config を shareable! 呼び出し
  ActionController::Base.descendants.each do |controller|
    controller.config.shareable! if controller.respond_to?(:config) && controller.config.respond_to?(:shareable!)
  end

  # 既存: Ractor 用の最終セットアップ
end

実際のコードは多少違う可能性がありますが、趣旨としては:

  • Rails が ractorize! するタイミングで
  • コントローラクラスが持つ設定(config など)を
  • Ractor 間で共有可能なオブジェクトに変換 or マークする

というものです。

PR本文にもあるように、「freeze + copy-on-write」方式は採用していません。代わりに「アプリ起動後はコントローラレベルの config は基本的に不変」という前提で shareable にしており、インスタンス側での変更は各インスタンスが持つ継承済みコピーで行うという設計に寄せています。


2-3. ActiveSupport::OrderedOptions に shareable 対応テストを追加

activesupport/test/ordered_options_test.rb に +10行のテストが追加されています。

意図としては:

  • Rails の設定の多くが ActiveSupport::OrderedOptions を使っているため、
    OrderedOptions 自体が「shareable 対応」できているかをテストした。
  • 例として考えられるテストイメージ:
ruby
def test_ordered_options_can_be_shareable
  opts = ActiveSupport::OrderedOptions.new
  opts.foo = "bar"

  opts.shareable! # もしくは、Ractor.shareable?(opts) が true になるように処理

  assert Ractor.shareable?(opts)
end

実際のメソッド名/API は PR によりますが、

  • OrderedOptions インスタンスに対して
  • Ractor 観点でも安全な状態にする API を呼び
  • Ractor から shareable? 判定しても通ること

が確認されるテストが追加されています。

これにより、コントローラ設定に限らず、Rails が内部で使っている OrderedOptions ベースの設定が Ractor 化に耐えられることを保証します。


2-4. ractors_test にコントローラ設定の shareable テスト

railties/test/application/ractors_test.rb に +9行のテストが追加されています。

ここでは:

  • Rails.application.ractorize! 実行後に
  • controller configs が実際に shareable になっているかを確認するテストが入っています。

想定されるテストイメージ:

ruby
test "controller configs are shareable after ractorize" do
  app("production") # テスト用のアプリ起動

  Rails.application.ractorize!

  assert Ractor.shareable?(ActionController::Base.config)
  # または特定のアプリケーションコントローラの config を確認
end

これにより、「ractorize! を呼んだあとで、まだ unshareable な controller config が残っていないか」を回帰テストとして担保しています。


  1. 影響範囲・注意点

3-1. コントローラレベルの設定を「起動後に書き換える」コードへの影響

PRの説明にも明記されている通り、

controller-level configs shouldn't be mutated once the app has booted.
Especially since controller instances have their own inherited copy of the config to mutate.

という設計思想に寄せられています。

つまり:

  • アプリケーションがブートした後(本番環境でプロセスが立ち上がった後など)に
  • コントローラクラスの config を直接変更するようなコード

は、Ractor 環境下では問題になる可能性があります(shareable オブジェクトを mutate しようとしてエラー、あるいは想定外の動作)。
例:

ruby
# 悪い例(本番起動後や ractorize! 後にやるべきでない)
PostsController.config.some_option = :new_value

このような用途が必要な場合は:

  • コントローラインスタンスに紐づく設定(@config 的なもの)があるならそちらを変更する
  • あるいは「実行時フラグ」「Feature flag」など別の仕組みを使う

といった方向が推奨されます。

3-2. Ractor を使わないアプリへの影響

  • 変更箇所は主に Rails.application.ractorize! 経由のパス、および shareable テストの追加であり、
  • 通常の(Ractor を使わない)Rails アプリでは基本的に挙動は変わらないはずです。
  • ただし、将来的な Ractor 対応を見据え、**「ブート後にクラスレベルの設定をむやみに書き換えない」**という前提が一層強くなった、と理解しておくと良いです。

3-3. マルチプロセス / スレッド環境との関係

  • 以前から推奨されていた「起動後にグローバルな設定オブジェクトをいじらない」方針と整合的です。
  • Ractor 特有の shareable 制約が明文化された形になるため、スレッドセーフ設計との相性もよく、「設定は起動時確定、以後は読み取り専用」という構成に寄せると安全です。

  1. 参考情報 (あれば)
  • Ruby Ractor 公式ドキュメント(shareable オブジェクトの概念など)
    https://docs.ruby-lang.org/ja/latest/doc/spec=2fractor.html
  • ActiveSupport::OrderedOptions
    Rails の設定オブジェクト(config.x.xxx など)でよく使われるクラスで、ハッシュライクに任意キーを格納できる。
  • 類似の PR / 方針
    • Rails の Ractor 対応系 PR は、ractorize! まわりで「設定を shareable にする」「Lazy ロードを排除する」といった変更が継続的に入っているため、同系列の1つと位置づけられます。

このPRにより、「Ractor化した Rails アプリでコントローラ設定を安全に共有できる」基盤が整ったと整理できます。


#58653 Freeze Active Record specific on callbacks

マージ日: 2026/9/2 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Active Record のトランザクション系コールバック(after_create_commit などの on オプション付き)を Ractor でも共有可能(shareable)にするため、これらのコールバック定義を freeze する変更です。これにより、Ractor モード :raise でアプリ起動時に落ちてしまう問題が解消されます。

  1. 変更内容の詳細

背景

ruby
class Post < ApplicationRecord
  after_create_commit :foo
end

上記のような「トランザクション完了時系」のコールバックは、内部的には Active Record のトランザクション機構 (ActiveRecord::Transactions) が on: :create などのオプション付きでコールバックを登録しています。

Ruby の Ractor では「共有不可能なオブジェクト」が Ractor 間にまたがると例外になります。Rails のブート時に eager load されたモデル定義が Ractor で評価される場合、コールバック定義に含まれるオブジェクトが shareable でないと、Ractor モード :raise では起動時にクラッシュします。

この PR は、その中でも Active Record 特有の on 付きコールバックが shareable になるように修正しています。

コードレベルの変更点

変更ファイルは 2 つです。

  1. activerecord/lib/active_record/transactions.rb

    • トランザクションコールバックの定義時に使われる on: オプション(on: :create, on: :update, on: :destroy など)を freeze するような修正が入っています。

    • 差分は +2/-2 行と小さいため、典型的には次のようなイメージの変更になっています(擬似コード):

      ruby
      # 変更前(イメージ)
      set_callback :commit, :after, method, if: ..., on: [:create, :update]
      
      # 変更後(イメージ)
      set_callback :commit, :after, method, if: ..., on: [:create, :update].freeze

      実際には、on に渡すシンボル/配列などのオプションオブジェクトを凍結するような変更です。
      freeze することで、そのオブジェクトが Ruby の shareable オブジェクトと見なされ、Ractor から安全に参照できるようになります。

  2. activerecord/test/cases/transaction_callbacks_test.rb

    • テストが 19 行追加されており、以下のような点を確認していると考えられます:
      • トランザクション系のコールバック (after_commit, after_create_commit, after_update_commit, after_destroy_commit など) に設定される on オプションが freeze されていること。
      • freeze されても、従来通りコールバックが正しいタイミングで呼ばれること(機能面に影響がないこと)。

  1. 影響範囲・注意点
  • 対象となる機能

    • Active Record のトランザクション系コールバック全般
      after_commit, after_rollback, after_create_commit, after_update_commit, after_destroy_commit など)
    • 特に on: ... オプションを内部的に利用している部分。
  • アプリケーションコードへの影響

    • 通常の利用(after_create_commit :foo など)では挙動は変わりません。

    • ただし コールバック定義時に渡されるオプションオブジェクトを後からミューテート(破壊的変更)していた場合、freeze により FrozenError になる可能性があります。
      例:(やってはいけないが、もしやっていた場合)

      ruby
      options = { on: [:create] }
      after_commit :foo, options
      options[:on] << :update  # ここで FrozenError になるようになる可能性

      そもそもこのような使い方は非推奨であり、Rails 的にも不変(immutable)である前提のほうが望ましいため、この変更は「仕様として筋が良い」方向です。

  • Ractor を使っていないアプリ

    • 実質的な挙動変更はほぼなく、パフォーマンスや互換性への影響は軽微と考えられます。
  • Ractor モードを使う/検証したいアプリ

    • Ractor モード :raise を有効にしている/検証している環境では、トランザクションコールバックを持つモデルが eager load されてもクラッシュしなくなります。
    • Ractor 対応を進める上での前提条件(Active Record コールバックが shareable であること)が一つ満たされます。

  1. 参考情報 (あれば)

この PR は小さい差分ですが、Rails の Ractor 対応を一歩進めるための基盤的な変更と位置付けられます。


#58639 Preserve fetch_multi key order when local cache is active

マージ日: 2026/9/2 | 作成者: @moizafzal936

  1. 概要 (1-2文で)
    ActiveSupport::Cache::Store#fetch_multi が「渡したキー順で結果を返す」という仕様を、ローカルキャッシュ (Strategy::LocalCache) 使用時にも正しく守るようにしたバグ修正です。ローカルキャッシュ命中/ミスの有無に関わらず、呼び出し側が指定したキー順で結果が返るようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveSupport::Cache::Store#fetch_multi は、ドキュメント上「引数に渡したキーの順序で値を返す」と明記されています (#34700 で導入)。
しかし Strategy::LocalCache が有効な場合のオーバーライド実装では、次のような流れになっていました:

  1. ローカルキャッシュに既に存在するキー(ヒット)を先に results に詰める
  2. ローカルキャッシュに無いキー(ミス)だけを super(下位ストアの fetch_multi)で取得して results にマージ

それぞれのステップ単体では順序は保たれているものの、「ヒットしたキー群 → ミスしたキー群」という順番で連結されるため、呼び出し元が渡したキーの順序とは異なる並びで返ってしまうケースがありました。

例:

ruby
cache.with_local_cache do
  cache.increment("bar")              # "bar" がローカルキャッシュに乗る
  cache.fetch_multi("foo", "bar") { 0 }.keys
  # 期待: ["foo", "bar"]
  # 実際(修正前): ["bar", "foo"]  # ローカルヒットの "bar" が先頭に来てしまう
end

どう直したか

ローカルキャッシュ付きの fetch_multi の返り値を、最終的に呼び出し元が指定したキー順で並べ直すように変更しています。

具体的には、ローカルキャッシュ分と下位ストア分をマージした後のハッシュに対して slice(*keys) を呼び出し、元のキー配列の順序に従って再構成しています。slice は指定したキーのみを順序どおりに取り出すメソッドのため、

  • 結果に含まれるキー集合は変わらない
  • skip_nil オプションや「ミスの記録」などの挙動は変えない

という条件を満たしたまま、順序だけを修正できます。

イメージ:

ruby
# 擬似コードイメージ(実際のPRは1行の置き換え)
def fetch_multi(*names, **options)
  # hits: ローカルキャッシュにある分
  # misses: 下位ストアから取ってきた分
  merged = hits.merge(misses)
  # ここで渡されたキー順に並べ直す
  merged.slice(*names)
end

これにより、Strategy::LocalCache 経由でも、ベース実装 Store#fetch_multi と同じく「リクエストしたキー順で返す」という保証が得られます。

テストとドキュメント

  • activesupport/test/cache/behaviors/local_cache_behavior.rb
    • 共有の LocalCacheBehavior モジュールに回帰テストを追加
    • すべてのローカルキャッシュ対応ストア(たとえば Redis, MemCache)で同じテストが動くようになっています
    • テスト内容のポイント:
      • increment を使ってローカルキャッシュにだけ値を載せる(ストア種別に依存しない手段)
      • 先頭・末尾キーはローカルミス、中間のキーだけローカルヒットになるようにし、その順序が乱れないことを検証
  • activesupport/CHANGELOG.md
    • fetch_multi のキー順序がローカルキャッシュ使用時にも保存されるようになったことを明記

なお、Issue 側で挙げられていた「write_multi の後に fetch_multi をすると、古い値が返ってくるのでは」という懸念については、PR説明によると仕様どおりであり、本PRでは順序の問題のみにフォーカスしています。


  1. 影響範囲・注意点
  • 影響範囲

    • cache.with_local_cache などでローカルキャッシュを有効化した状態で fetch_multi を使っているコード全般
    • Redis キャッシュストア、MemCache など Strategy::LocalCache をミックスインしているストアが対象
  • 互換性

    • キーの「どれが含まれるか」や取得される値自体のロジック(ヒット/ミス判定、skip_nil、ミスの記録など)は変わりません
    • 変わるのは返り値のハッシュのキー順のみです
      • これまで「ローカルにヒットしたキーが先に並ぶ」実装に依存していた場合は、順序が変わることに注意してください
      • 正しくは「渡したキー順で返る」ことに依存すべきであり、今回の修正はドキュメントどおりの挙動への是正です
  • テスト

    • 既存のテストに加え、回帰テストが共通のBehaviorとして入っているため、今後別ストアの変更などによる再発リスクは低減されています

  1. 参考情報 (あれば)
  • 対応Issue: #58637
  • 元仕様導入PR: #34700 (fetch_multi がキー順を保証するようになった変更)
  • 関連クラス:
    • ActiveSupport::Cache::Store#fetch_multi
    • ActiveSupport::Cache::Strategy::LocalCache

#58642 Freeze Active Record time zone related configuration

マージ日: 2026/9/2 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Rails 7 での Ractor 対応の一環として、Active Record の「タイムゾーン関連設定」をフリーズ(凍結)し、Ractor 間の共有時に起きる不変性違反・Isolation 問題を解消する PR です。これにより、属性セット(attribute set)が Ractor ごとに遅延構築されても、安全に設定を共有できるようになります。

  1. 変更内容の詳細

背景・問題点

  • #58578 により、Active Record の「attribute set(カラム定義などを含むメタ情報)」は:

    • 以前: load_schema 時に一括・事前ロード(eager load)
    • 現在: Ractor ごとに遅延構築(lazy, per ractor)
  • その結果、次のようなコードが動かなくなっていました:

    ruby
    Post.create!
    
    Ractor.new do
      Post.create!
    end

    理由:

    • Ractor 内で attribute set が再構築される過程で、Active Record の「タイムゾーン関連設定」(例: config.active_record.default_timezonetime_zone_aware_attributes など)にアクセスする。
    • これらの設定オブジェクトが Ractor-safe(不変 / deep-freeze)ではないため、Ractor 間で共有すると isolation ルールに違反してしまう。

この PR の対応内容

  • Active Record Railtie (activerecord/lib/active_record/railtie.rb) において、
    • 「Active Record のタイムゾーン関連設定」をアプリケーション起動時に**freeze(凍結)**するように変更。
    • これにより、その設定オブジェクトは不変になり、Ractor 間で安全に共有可能になる。
  • railties/test/application/configuration_test.rb にテストを追加し、
    • 該当の設定が起動時にフリーズされていること、
    • かつ通常利用に支障がないこと を確認している。

※ PR 本文から読み取れる範囲では「どの設定キーをフリーズしているか」の列挙はありませんが、通常は以下のような Active Record のタイムゾーン系設定が対象になります(イメージ):

  • config.active_record.default_timezone
  • config.active_record.time_zone_aware_attributes
  • config.active_record.time_zone_aware_types
  • config.active_record.active_record.default_timezone 周辺の内部設定

実際のコードでは、これらの設定を参照している内部構造(配列・ハッシュなど)もフリーズ対象にしていると考えられます。


  1. 影響範囲・注意点

影響範囲

  1. Ractor を使う Rails アプリ

    • Ractor 内で Active Record モデルを初回利用する際に attribute set が構築されても、タイムゾーン設定が Ractor-safe になったことでエラーなく動作するようになります。
    • とくに並列処理で Ractor.new { Post.create! } などを行うケースが対象。
  2. Active Record のタイムゾーン設定を実行時に書き換えていたコード

    • 起動後に config.active_record.xxx = ... 相当のことを行う / 内部構造を変更するようなコードがあった場合、freeze により FrozenError が発生する可能性があります。
    • そのため、タイムゾーン関連設定は「起動前(configuration phase)」にのみ変更することが前提になります。

注意点

  • 設定を書き換えるパターンの例(NG になりうる):
    ruby
    # 起動後どこかで
    ActiveRecord::Base.time_zone_aware_attributes = false  # これが禁止/例外になる可能性あり
    ActiveRecord::Base.time_zone_aware_types << :datetime   # フリーズされた配列の破壊的変更は不可
  • 対策としては:
    • config/application.rb もしくは各 config/environments/*.rb 内で、
      ruby
      config.active_record.time_zone_aware_attributes = false
      config.active_record.time_zone_aware_types = [:datetime, :time]
      のようにアプリ起動前にすべて設定しておく必要があります。
  • Ractor を使わない・設定を書き換えない通常のアプリでは、基本的に挙動の変化はなく、パフォーマンスへの影響も軽微です(freeze は起動時に一度行われるだけ)。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #58578: attribute set を「Ractor ごとに遅延構築」に変更した PR
  • Ractor 関連の一般的な注意:
    • Ractor 間で共有できるのは「イミュータブル(freeze 済み)」なオブジェクトか、一部の Ractor-safe オブジェクトのみ。
    • Rails の設定オブジェクトや内部配列/ハッシュを Ractor で共有するには、今回のような freeze 対応が必須になる。

#58627 Make ActionPack settings instance variables

マージ日: 2026/9/2 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionPack 内の一部設定を mattr_accessor ベースのクラス変数管理から、インスタンス変数ベースの設定管理に切り替える変更です。特に ActionDispatch::Request の MIME ネゴシエーション設定(ignore_accept_header)などで、「インスタンスからクラス設定が書き換わる」不自然な挙動をやめ、より一貫した設定の扱いにしています。

  1. 変更内容の詳細

2-1. MimeNegotiation (mime_negotiation.rb)

元の挙動の例として PR で挙げられているのが以下です:

ruby
ActionDispatch::Request.ignore_accept_header
# => false

ActionDispatch::Request.new({}).ignore_accept_header = true
# => true

ActionDispatch::Request.ignore_accept_header
# => true

ここでは ignore_accept_headermattr_accessor で定義されていたため、

  • クラスレベル: ActionDispatch::Request.ignore_accept_header
  • インスタンスレベル: ActionDispatch::Request.new({}).ignore_accept_header = true

の両方から同じクラス変数を触っており、インスタンスの設定変更がそのままクラス設定を書き換えてしまう状態でした。

PR では:

  • mattr_accessor をやめて、ActionPack の「設定オブジェクト」(ActionPack::Railtie がぶら下げている config)に紐づくインスタンス変数として扱うように変更
  • それに伴って、MimeNegotiation では インスタンスレベルのアクセサを提供しない 形に整理
    (PR 本文で「インスタンスレベル accessors を消すが、非公開 API かつインスタンスからクラスを間接的に変えるのはおかしいので問題ない」と明言)

結果として:

  • ActionDispatch::Request.ignore_accept_header のような「グローバル設定」は Rails 設定オブジェクト由来のインスタンス変数で一元管理される
  • request.ignore_accept_header = true のように、個々の Request インスタンスからグローバル設定を書き換えるパターンは廃止される(そもそも公開 API ではなかった)

といった整理が行われています。

2-2. ExceptionWrapper (exception_wrapper.rb)

ExceptionWrapper 周りの設定も同様に、「クラス変数 / mattr_accessor ベース」から「インスタンス変数ベースの設定」に寄せる変更が入っています。

典型的には以下のような設定が対象と考えられます(実際のコードは PR ファイル内ですが、Rails 既存仕様からの類推です):

  • backtrace の表示有無
  • 内部エラーかどうかの判定に使うクラス・ステータスコードのマッピング

これらが:

  • 以前: mattr_accessor でクラス単位のグローバル設定
  • 以後: ActionPack 設定オブジェクトに紐づいたインスタンス変数で管理される

ようになります。
テスト (exception_wrapper_test.rb) では、例外ラップ時の設定反映のしかたやデフォルト値がインスタンス変数経由でも正しく動作することを検証しています。

2-3. テストの追加 (request_test.rb, exception_wrapper_test.rb)

  • ActionDispatch::Request が新しい設定保持方法でも期待通り動くことを確認するテストが追加
  • ExceptionWrapper についても、例外発生〜ラップ〜レスポンス生成の流れで、設定が正しく適用されるかを確認するテストが追加

これにより:

  • インスタンス変数ベースへの移行による挙動変化が意図通りであること
  • 互換性の維持(公開 API ベースで見たときに問題がないこと)

がテストで裏づけられています。


  1. 影響範囲・注意点

3-1. インスタンスからクラス設定を書き換えるコードは壊れる可能性

次のようなコードを書いている場合は動かなくなる/意味が変わる可能性が高いです:

ruby
# 非推奨な使い方の例(元々ドキュメント化されていない)
request = ActionDispatch::Request.new(env)
request.ignore_accept_header = true  # これでグローバル設定が変わることに依存している

PR ではこのパターンを「非ドキュメントな挙動」と位置づけており、ここへの依存はサポート対象外として切り捨てています。
クラス全体の設定変更はクラス/設定オブジェクト経由でのみ行うようにコードを見直す必要があります。

3-2. 設定のライフサイクルとスレッド安全性

mattr_accessor + クラス変数は、マルチスレッド環境で扱いが難しく、かつ複数アプリケーション/エンジンから共有される場合に意図せず衝突することがあります。

インスタンス変数ベースへの移行は:

  • Rails の設定オブジェクト単位にスコープが閉じる
  • テスト時に設定を差し替えたり、1 プロセス内で複数アプリを動かしたりするケースでの独立性が上がる

といったメリットがあり、これに合わせてコード側でも「設定は config/initializer 経由で一元管理する」書き方に寄せていくのが望ましいです。

3-3. 公開 API と非公開 API の線引き

今回削られたのは主に:

  • インスタンスから ActionPack のクラス設定をいじるためのアクセサ
  • もともとドキュメント化されていない(= Rails チームとして互換性を保証していない)部分

です。
もし自前で ActionDispatch::RequestExceptionWrapper を直接拡張・モンキーパッチしている場合は、この PR の差分を一度確認して、クラス変数/mattr_accessor に直接依存していないかをチェックする必要があります。


  1. 参考情報 (あれば)
  • この PR は #58620 と同系統の変更で、ActionPack 全体で「設定をインスタンス変数ベースに寄せる」リファクタリングの一部と位置付けられています。
  • 関連するコードは主に以下:
    • actionpack/lib/action_dispatch/http/mime_negotiation.rb
    • actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
    • テスト:
      • actionpack/test/dispatch/request_test.rb
      • actionpack/test/dispatch/exception_wrapper_test.rb
  • Rails の設定まわりの設計方針として、「mattr_accessor / クラス変数を減らし、フレームワーク設定オブジェクトとインスタンス変数側に寄せる」流れが続いており、この PR もその一環です。

#58643 Store the AssetTagHelper settings in singleton class attributes

マージ日: 2026/9/2 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    AssetTagHelper が持つ6つの設定値の保持方法を、クラス変数からモジュール自身のインスタンス変数(単一オブジェクト上の属性)へ変更し、インスタンス経由でグローバル設定を書き換える仕組みを削除して、Ractor 対応と設計の明確化を図る変更です。Rails 7以降で予定される Ruby 4.0 / Ractor 環境でも AssetTagHelper の設定が安全に参照できるようにすることが主目的です。

  1. 変更内容の詳細

2-1. 何が問題だったか

  • 対象: ActionView::Helpers::AssetTagHelper
  • ここには「6つの設定」があり、従来は クラス変数 (@@foo など) で保持されていました。
  • Ruby 4.0 では、メイン Ractor 以外からクラス変数を読むことができない 仕様になる予定であり、
    AssetTagHelper の設定が非メイン Ractor から参照できず、Ractor 対応に支障が出る状態でした。
  • さらに、「ヘルパーインスタンスの setter を呼ぶとグローバル設定を書き換える」というような設計になっており、
    どこから設定が変更されるか分かりづらく、副作用も大きい状態でした。

2-2. どう直したか

1) クラス変数 → モジュールのインスタンス変数へ

  • AssetTagHelper モジュール自体は Ruby のオブジェクトなので、そこに インスタンス変数 (@foo) を持たせることで
    「事実上のシングルトン設定オブジェクト」として扱うように変更されています。
  • これにより:
    • Ractor 間でクラス変数を共有しない Ruby 4.0 の仕様に依存せず、
      「モジュールオブジェクトの属性」という形で状態を保持できます。
    • グローバルな設定は依然として 1 箇所にまとまっているが、保持方法が Ractor 互換的になります。

※ PR本文では「Use instance variables on the module directly」と記載されている箇所がこれに該当。

2) 「委譲用の reader メソッド」を追加

  • 実際に image_tag, javascript_include_tag などのアセットタグヘルパーからは、
    直接インスタンス変数にアクセスするのではなく、reader メソッドを経由して設定値を取得するように変更。

  • 構造イメージ(擬似コード):

    ruby
    module ActionView
      module Helpers
        module AssetTagHelper
          class << self
            attr_accessor :asset_path, :asset_host, :asset_url_options
            # ... 他の3つの設定も同様
          end
    
          # インスタンスから呼ばれる reader
          def compute_asset_host(...)
            AssetTagHelper.asset_host # ← モジュールのクラスメソッド経由
          end
        end
      end
    end

    ※実際の名前や数はPRの「six settings」に相当するものですが、構造イメージとして。

  • これにより、「どの設定がどこから参照されているか」が明確になり、
    かつ Ractor 互換な形で設定にアクセスできます。

3) インスタンス writer による「グローバル書き換え」を廃止

  • これまで AssetTagHelper には、「インスタンスメソッドの writer を呼ぶと内部的にクラス変数を書き換え、
    それがグローバル設定になる」というパターンがありました。

    例(イメージ):

    ruby
    # 以前はこういうことができた可能性がある:
    helper.asset_host = "https://cdn.example.com"  # 実はグローバルを書き換えていた
  • PRでは、このようなインスタンス writer が削除されています。

    • これにより、「個々の view コンテキストでの操作が、実はグローバル設定を壊していた」 といった曖昧な挙動がなくなります。
    • グローバルな設定変更は、config.action_controller.asset_host = ...
      Rails.application.config.action_controller.asset_host = ... のような正式な設定経路を使うのが前提になります。

4) テストの追加

  • actionview/test/template/asset_tag_helper_test.rb に 28 行のテストが追加。
  • 主な意図:
    • 新しい設定保持方式で、従来どおりの挙動(設定値の参照結果)が維持されているか
    • インスタンス writer の削除に伴う API の変化で、想定どおりにエラー/挙動になるか
  • これにより後続の Ractor 対応や Ruby バージョンアップでのリグレッションを防ぎます。

  1. 影響範囲・注意点

3-1. 公開API的な影響

  • 一般的な Rails アプリで普通に image_tag, stylesheet_link_tag などを使っているだけなら、
    挙動の変化は基本的にありません

  • 影響しうるのは、以下のようなコードを書いている場合です:

    • ActionView::Helpers::AssetTagHelperクラス変数 (@@...) を直接参照・変更していた
    • AssetTagHelper の インスタンスメソッドの setter を使ってグローバル設定を書き換えていた(非推奨な内部実装依存)。

    これらはもともと内部実装への依存度が高い書き方であり、Rails の想定する使い方ではありません。 そのため、この PR によりコードが動かなくなる可能性があります。

3-2. Ractor / 並行実行環境での意味

  • Ruby 4.0 での Ractor を使った並行実行環境では、
    クラス変数はメイン Ractor 以外から読めなくなります。
  • 本変更により、AssetTagHelper の設定は:
    • モジュールオブジェクト上のインスタンス変数として一元管理される
    • reader メソッド越しに参照される
  • これが Ractor 間でどう共有/コピーされるかは、さらに上位レイヤ(設定の freeze・複製戦略など)にも依存しますが、
    少なくとも「クラス変数が読めなくて落ちる」という問題は避けられるようになります。
  • 将来的に Rails が Ractor フレンドリーな構成に進んでいく上での前提整備的な変更といえます。

3-3. gem / ライブラリ作者への注意

  • AssetTagHelper をモンキーパッチしてクラス変数を書き換えていた / 参照していた gem がある場合は、
    これを機に 公式にサポートされる設定 APIRails.application.config.action_controller.asset_host など)への
    依存に切り替える必要があります。
  • AssetTagHelper の内部実装(変数の種類や場所)に直接依存するのは今後ますます壊れやすくなるため、
    本PRは「内部実装への依存をやめてほしい」というメッセージとも解釈できます。

  1. 参考情報 (あれば)
  • 本PRが参照している前段PR:
    • https://github.com/rails/rails/pull/58627
      → 同様に「グローバル設定をクラス変数で持ち、インスタンス writer がグローバルを書き換える」ような箇所を
      Ractor 互換な形へリファクタリングしていると考えられます。
  • Ractor とクラス変数の制約(Ruby 側の事情):
    • Ruby 3 系以降、Ractor ではグローバル可変状態の扱いが厳しくなっており、 クラス変数やグローバル変数は基本的に Ractor 間で安全に共有できない方向の設計です。
    • Rails はこの流れに合わせ、設定値の持ち方を徐々に Ractor フレンドリーな形へ移行しています。

#58629 Make the allow_browser callback shareable

マージ日: 2026/9/2 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    allow_browser の「未サポートブラウザに対するデフォルト動作」が Ractor でも共有可能なオブジェクト(shareable proc)になるよう修正された PR です。あわせて、Ruby の制約に対応するため、オプションキーワード引数 :block の扱いが変更されています。

  1. 変更内容の詳細

2-1. 背景: Ractor 対応と shareable なオブジェクト

Ruby 3 以降の Ractor では、「Ractor 間で共有できるオブジェクト (shareable)」と「共有できないオブジェクト」が明確に区別されます。
allow_browser 内部で利用していた「未サポートブラウザ時のデフォルト動作」を表す Proc / lambda が shareable でない場合、Ractor からそのコントローラやミドルウェアを利用しようとするとエラーや制約にぶつかります。

この PR では、そのデフォルト動作を shareable な Proc として定義し直すことで、Ractor 内から allow_browser を使えるようにしています。

2-2. allow_browser のデフォルト動作を shareable に

actionpack/lib/action_controller/metal/allow_browser.rb において、未サポートブラウザに対するデフォルトのコールバック(たとえば 406 を返す、または何らかのエラー/レスポンスを返す処理)が、Ractor でも共有可能な Proc/Lambda として定義し直されています。

コード断片イメージ(※実際のコード構造のイメージです):

ruby
# もともとクラス定義内のメソッドで直接 Proc を生成していたところを、
# Ractor.shareable な形で定義するように変更
DEFAULT_UNSUPPORTED_BROWSER_ACTION = lambda do |controller|
  # 未サポートブラウザ時のデフォルトのレスポンス処理
end
Ractor.make_shareable(DEFAULT_UNSUPPORTED_BROWSER_ACTION) if defined?(Ractor)

allow_browser を呼び出すときにブロックを渡さなかった場合、この shareable な DEFAULT_UNSUPPORTED_BROWSER_ACTION が使われるような方針になっています。

2-3. オプションキーワード引数 :block のローカル変数化

PR で説明されている通り:

I also had to capture the optional keyword :block as a separate local variable to allow the lambda to capture it. It's a limitation with Ruby detecting optional keywords with non-literal default values as reassignable.

Ruby では、「非リテラルなデフォルト値を持つオプションキーワード引数」は「再代入される可能性がある」と見なされ、lambda 内でのキャプチャに制限がかかるという仕様上の制約があります。

そのため、以下のような形の修正が入っています。

ruby
def allow_browser(block: DEFAULT_UNSUPPORTED_BROWSER_ACTION, **options)
  # 直接 block を lambda からキャプチャすると問題が出るため、
  # 一度ローカル変数に退避する
  action = block

  before_action(**options) do
    # この lambda / proc が action (block) をキャプチャする
    instance_exec(&action)
  end
end

ポイント:

  • キーワード引数 block: を直接 lambda/Proc が参照するのではなく、
    一度 action などのローカル変数に代入してからキャプチャする。
  • これにより Ruby の「非リテラルなデフォルト値を持つキーワード引数は再代入可能」という判定を回避しつつ、Ractor shareable な形で Proc を取り回せるようにしている。

2-4. テストの追加

actionpack/test/controller/allow_browser_test.rb に 23 行のテストが追加されています。
主に以下の点を検証していると考えられます。

  • ブロック未指定で allow_browser を呼び出した場合も正常に動作すること。
  • デフォルトの unsupported-browser コールバックが期待通りのレスポンスを返すこと。
  • Ractor や shareable 化に起因する動作の回 regressions がないこと。

(テスト名の構成から、少なくともデフォルト動作の利用パスや、block: キーワードの扱いまわりがカバーされているはずです。)


  1. 影響範囲・注意点
  • 通常のシングルスレッド / マルチスレッド Rails アプリ
    既存の allow_browser 利用コードは、基本的に挙動はそのままです。
    ブロックを明示的に渡している場合も、渡していない場合も、API のシグネチャは変わっていません。

  • Ractor を利用する環境での改善

    • allow_browser を使ったコントローラやミドルウェアが、Ractor 内でも利用しやすくなります。
    • 従来は Ractor で ActionController を扱うときに、内部に non-shareable な Proc があって問題になる可能性がありましたが、その一部が解消されています。
  • カスタムの unsupported-browser コールバックを渡している場合

    • 自分で allow_browser block: -> { ... } のように Proc/Lambda を渡している場合、その Proc/Lambda 自体が shareable でないと、Ractor 間で共有する際には依然として制限を受けます。
    • この PR が shareable にしたのは「Rails のデフォルト動作」であり、ユーザー定義の Proc までは自動的に shareable にはなりません。
    • Ractor をガチ利用する場合は、自分のブロックも Ractor.shareable? を意識した実装にする必要があります。
  • Ruby の仕様に依存した書き方のため、バージョン依存の可能性
    「オプションキーワードの非リテラルデフォルト値」に関する Ruby の挙動に対処する実装となっているため、Ruby の将来のバージョンでこの仕様が変わると、また別の書き方が必要になる可能性があります。ただし現時点では Rails 側で安全なパターンに寄せているため、実務的には問題になりにくい想定です。


  1. 参考情報 (あれば)
  • 変更ファイル

    • actionpack/lib/action_controller/metal/allow_browser.rb
    • actionpack/test/controller/allow_browser_test.rb
  • 関連ドキュメント

    • Rails ガイド (Edge Guides): Action Controller Overview / Browser support / allow_browser セクション(最新版に追従される可能性があります)
    • Ruby 3+ Ractor ドキュメント: Ractor.shareable?, Ractor.make_shareable
    • Ruby のキーワード引数とデフォルト値に関する仕様:
      • 非リテラルなデフォルト値の扱い
      • lambda / Proc へのキャプチャ時の制約

#58645 Fix TypeCaster sometimes leaking connection

マージ日: 2026/9/2 | 作成者: @skipkayhil

  1. 概要 (1-2文で)
    TypeCaster が内部で使う DB コネクションを「借りっぱなし」にしてしまうケースがあり、接続プールからコネクションがリークする不具合を修正した PRです。Executor によるクリーンアップに依存せず、with_connection を使って即座にコネクションを返却するように変更されています。

  1. 変更内容の詳細

変更ファイルは 1 件で、実質 1 行のメソッド呼び出し差し替えです。

activerecord/lib/active_record/type_caster/connection.rb 内で、TypeCaster がデータベース接続を取得する際のコードが、(推測される形として)以下のように変わっています:

ruby
# 変更前(例示)
def with_connection
  @connection_pool.connection.tap do |conn|
    yield conn
  ensure
    # Executor 終了時のクリーンアップに依存していた
  end
end

# 変更後(例示)
def with_connection
  @connection_pool.with_connection do |conn|
    yield conn
  end
end

実際の差分としては +1 / -1 行なので、だいたい次のような単純な呼び出し変更です:

ruby
# before
connection = @pool.connection

# after
@pool.with_connection do |connection|
  ...
end

ポイントは「connection で取得してそのまま使う」のではなく、「with_connection ブロックを通して使う」ようにしたことです。with_connection はブロックを抜けるタイミングで確実に connection_pool にコネクションを返却するため、Executor(スレッド実行コンテキスト)に紐づくクリーンアップ処理に頼らずともリークしない形になります。

PR の説明にある通り、「Live actions(ActionController::Live / Turbo Streams など、長時間生きる処理のコンテキスト)では Executor のクリーンアップが期待通りに呼ばれないことがある」ため、その前提に依存していた TypeCaster 経由のコネクション利用がリークの原因になっていました。この PR によって TypeCaster 側だけで完結してコネクションの貸し出し/返却を管理するように修正されています。


  1. 影響範囲・注意点
  • 影響範囲

    • ActiveRecord の TypeCaster が DB にアクセスして型情報を得るようなケース(例: カラム情報の解決など)で、内部的に利用される DB コネクションの扱いが変わります。
    • 特に Live actions やストリーミングレスポンスなど「リクエストライフサイクルが長い」コンテキストで、接続プールのコネクション枯渇やリークが発生していた環境で効果が出ます。
    • 通常のリクエスト/レスポンスでも、より確実にコネクションがプールに戻る形になるため、コネクション使用の健全性が高まります。
  • 注意点

    • 互換性への影響はほぼありません。API シグネチャの変更はなく、内部実装の改善に留まっています。
    • もし独自に TypeCaster まわりを拡張して connection_pool.connection 取得をラップしているようなメタプログラミングをしている場合は、with_connection 前提の実装になったことを念のため確認するとよいです(通常はそのようなケースは稀です)。
    • コネクションが早めに返却されることにより、「同じスレッドがずっと同じコネクションを保持する」ことを前提にしたようなコードがあれば(これ自体がアンチパターンですが)、挙動が変わる可能性があります。

  1. 参考情報 (あれば)
  • 関連 Issue: #58644
    • 内容: TypeCaster 利用時にコネクションがリークし、特に Live actions で接続プールが枯渇する問題の報告。
  • Rails コネクションプール API:
    • ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection
      • ブロック内でコネクションを貸し出し、ブロック終了時に必ずプールへ返却する公式かつ推奨の使い方。
  • 背景技術:
    • Rails では Executor(ActiveSupport::Executor)を通じて、スレッドまたはリクエスト単位でのリソースライフサイクル管理(run! / complete!)を行っていますが、Live actions など特殊なライフサイクルではこれに完全には乗らないケースがあり、そのギャップを埋めるための修正と言えます。

#58578 Ractorize Active Record schema context

マージ日: 2026/9/2 | 作成者: @gmcgibbon

  1. 概要 (1–2文で)
    Active Record の SchemaContext を、Ractor(マルチスレッド/マルチRactor環境)でも安全に扱えるように再設計したPRです。SchemaContext の中に Attributes オブジェクトを埋め込み、スキーマ関連の状態を Ractor ごとに分離しつつ、モデルクラス側の ivar はメイン Ractor で遅延評価できるように整理しています。

  1. 変更内容の詳細

2-1. SchemaContext の構造変更

目的
SchemaContext が持つ属性関連の状態を、Ractor ごとに独立させるために、Ractor のストレージ内に埋め込める専用クラス (Attributes) に切り出しています。
これにより、同じモデルでも Ractor ごとに別のスキーマ状態を持てるようになり、Ractor セーフなスキーマ参照が可能になります。

主な変更点

  • ActiveRecord::ModelSchema::SchemaContext が次のような構造に変更:
    • これまではコンテキスト自身が直接 ivar で各種状態(attributes / primary key / predicate builder など)を持っていた
    • 変更後は SchemaContext::Attributes のようなオブジェクトを内部に保持し、それを Ractor ストレージに載せる設計に
  • 「スキーマコンテキストに固有なインスタンス変数」をモデルクラス側に移動し、メイン Ractor で遅延評価できるようにした
    例:
    • 某モデルクラスの @arel_table / @primary_key / predicate 関連キャッシュなど、これまで SchemaContext 側にあった(または強く結合していた)ものを、モデルクラスの ivar として保持+必要になった時点で計算
  • この設計により:
    • 「Ractor ローカルな状態」… SchemaContext::Attributes などを Ractor ストレージに置く
    • 「プロセス全体で共有しつつ、スレッドセーフかつ遅延評価するもの」… モデルクラスの ivar で扱う という切り分けが明確になります。

2-2. Active Model / Active Record 周辺の調整

変更ファイルから読み取れる範囲でのポイント:

  • activemodel/lib/active_model/attribute_registration.rb
    • 属性登録まわりで、SchemaContext の新しい設計(Attributes オブジェクト)に対応
    • モデルに属性を定義・登録する際に、Ractor ごとに正しい属性定義が取れるような参照経路に変更
  • activemodel/lib/active_model/type.rb
    • 型情報 (ActiveModel::Type) を扱う部分で、SchemaContext/Ractor 対応のための小さな修正
    • 例: type マッピングの参照先が SchemaContext 内の Attributes を経由するように調整されている可能性が高い
  • activerecord/lib/active_record/attributes.rb
    • モデルインスタンスの attributes 管理が、新しい SchemaContext 構造に追従
    • @attributes のビルドや読み込み時に、Ractor ごとのスキーマ状態を使うような経路に
  • activerecord/lib/active_record/model_schema.rb
    • モデルのスキーマ設定(table_name, primary_key, inheritance_column など)を扱うメソッドが、
      • Ractor ローカルな情報は SchemaContext
      • グローバルかつ遅延評価すべき情報はモデルクラス ivar に適切に分離されるよう処理をリファクタリング

※PR本文にコード例は無いですが、概念的には以下のようなイメージになります:

ruby
# 旧: SchemaContext が直接いろいろ持っていた
class SchemaContext
  def primary_key
    @primary_key ||= compute_primary_key
  end
end

# 新: Ractor ローカルな部分を Attributes に分離し、モデルクラスの ivar にも責務を移動
class SchemaContext
  class Attributes
    # Ractor ごとに変化しうる attribute 情報をここに持つ
  end

  def attributes
    @attributes ||= Attributes.new(...)
  end
end

class ApplicationRecord < ActiveRecord::Base
  class << self
    def primary_key
      @primary_key ||= schema_context.attributes.primary_key
    end
  end
end

2-3. テストまわりの変更

  • activerecord/test/cases/attributes_test.rb
    • 属性定義・読み書きロジックが、新しい SchemaContext 実装でも変わらないことを確認するように更新
    • 既存の仕様を壊していないかの回帰テスト寄りの変更
  • activerecord/test/cases/core_test.rb
  • activerecord/test/cases/primary_keys_test.rb
  • activerecord/test/cases/relation/predicate_builder_test.rb
    • 主キー解決・リレーション生成・predicate builder など、スキーマ情報を多用する部分について、
      • Ractor 対応後も今まで通り動作するか
      • Ractor ごとの状態差分を持てるか などを確認するテストが追加/更新
  • activesupport/lib/active_support/testing/ractors_assertions.rb
    • Ractor を使ったテストを支援する assertion 群に、追加アサーションが入っている
    • 例: 「複数 Ractor で同じコードを実行しても衝突しない」「Ractor 間で状態が混ざらない」ことを検証する機能

  1. 影響範囲・注意点
  • Ractor を使わないアプリケーション
    • 基本的には挙動は変わらない想定(後方互換性を維持するリファクタリング)
    • ただし、Active Record の内側の ivar 構造が変わっているため、
      • 内部 API(SchemaContext や内部 ivar)を直接触っている場合は影響しうる
  • Ractor / マルチスレッドで Active Record を使う場合
    • Ractor ごとに独立したスキーマ属性状態を持てるようになり、データ競合や不正なメモリアクセスのリスクが下がる
    • モデルのクラスレベル状態(スキーマ由来の ivar)はメイン Ractor で lazy に評価されるため、
      • 初回アクセス時にわずかなオーバーヘッドがある一方、
      • Ractor 作成時点で全モデルのスキーマを読み込む必要はなくなり、メモリと初期コストを抑えられる
  • 内部 API・メタプログラミングに依存しているコード
    • SchemaContext の具体的な実装(インスタンス変数構成やメソッド参照)に依存している場合、今回の設計変更で壊れる可能性がある
    • 公開 API(通常の Active Record モデルの使い方)内にとどまっていれば、変更は透過的なはず

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58578
  • 関連キーワード:
    • Ractor セーフな Active Record
    • SchemaContext / Attributes の責務分離
    • クラスレベル ivar の遅延評価 (lazy evaluation)
  • 背景知識:
    • Ruby Ractor: Ruby 3 以降で導入された並列実行機構。オブジェクト共有制限があるため、フレームワーク側で Ractor 対応のための設計変更が必要になる。

#58636 Remove leftover perform_query stubs in TrilogyAdapterTest

マージ日: 2026/9/2 | 作成者: @yahonda

  1. 概要 (1-2文で)
    TrilogyAdapterTest で perform_query をスタブしていたテストが、スタブ後に元のメソッドを正しく戻さずにコネクションプールへ返していたため、非同期クエリのリトライ系テストが期待どおりに失敗を注入できなくなっていました。
    このPRでは、スタブ終了後にコネクションインスタンスに残ってしまう perform_query のシングルトンメソッドを ensure ブロックで確実に削除し、後続テストへの副作用をなくしています。

  1. 変更内容の詳細

問題の背景

  • TrilogyAdapterTest 内の3つのテスト

    • #begin_db_transaction raises error
    • #commit_db_transaction raises error
    • #rollback_db_transaction raises error
      で、プールから取得したコネクションオブジェクトに対して perform_queryMinitest#stub で差し替えていました。
  • Minitest#stub は、対象インスタンスの シングルトンクラスにメソッドを定義する形で stub を差し込む ため、ブロック終了後に「元のメソッドに戻す」処理はしてくれるものの、

    • シングルトンクラスにエイリアスや元メソッドを残す
    • その結果として、そのインスタンスに「クラス定義よりも優先される perform_query がぶら下がった状態」
      になっていました。
  • 後続の AsynchronousQueriesTest では、「perform_query をアダプタクラスレベルで再定義」することで、意図的にクエリ失敗を発生させ、リトライ動作をテストしています:

    ruby
    with_async_query_failures do
      # perform_query をクラスレベルで上書きし、失敗 -> リトライを検証
    end
  • ところが、一部の接続インスタンスは前述の TrilogyAdapterTest によるシングルトンメソッドを保持したままプールに戻されており、その接続を掴んだスレッドでは

    • クラスレベルでの perform_query 再定義が インスタンスに届かない
    • つまり「失敗注入されない接続」で非同期テストが動いてしまう
      という状態になっていました。
  • どのテストが落ちるかは「どのスレッドがその汚染済み接続を取るか」に依存するため、再現性がシード値やCI環境に依存する、やや厄介なテスト汚染バグになっていました。

このPRでの修正内容

  • 上記3つのテストで、perform_query をスタブした後、必ずシングルトンメソッドを削除するよう修正しています。
  • 具体的には、テスト内の stub 利用箇所を ensure ブロックで囲み、connection.singleton_class.remove_method(:perform_query) のような形で後始末を行っています(実際のコードは trilogy_adapter_test.rb に6行追加)。

イメージとしては以下のような変更です(疑似コード):

ruby
def test_begin_db_transaction_raises_error
  connection = ActiveRecord::Base.connection_pool.checkout

  begin
    connection.stub(:perform_query, ->(*) { raise SomeError }) do
      # ここで begin_db_transaction の挙動を検証
    end
  ensure
    # stub によりインスタンスにぶら下がった perform_query を削除
    connection.singleton_class.send(:remove_method, :perform_query) if
      connection.singleton_class.method_defined?(:perform_query)
  end
end

これにより、テスト終了時にはコネクションインスタンスから perform_query のシングルトンメソッドが確実に消え、クラスレベルの再定義が正しく効くようになります。


  1. 影響範囲・注意点
  • 影響範囲は テストコードのみ であり、本番コード(ActiveRecord Trilogy Adapter 本体)の挙動には変更ありません。
  • ただし、これまで「たまたま通っていた」非同期クエリのリトライ周りのテストが、他のテストとの順序依存で落ちる可能性があったものが、順序に依存せず安定して動作するようになります。
  • 類似のパターン(インスタンスに対して stub して、接続プール等に返すもの)が他のアダプタ/テストにもある場合、同様の問題を引き起こす余地があります:
    • 接続オブジェクトやグローバルに再利用されるオブジェクトに対して stub を行う場合、
      • ブロック終了後のメソッド解決順序(シングルトンメソッドの残存)を意識する
      • 必要なら ensure での後片付けや、クラスレベルの stub/mocking を検討する
        ことが推奨されます。

  1. 参考情報 (あれば)
  • 関連PR: 非同期クエリのリトライテストを追加した PR #58049 のフォローアップ。
  • 再現条件:
    • TrilogyAdapterTest の該当テストが先に実行され、その後 AsynchronousQueriesTest のリトライ系テストを実行すると、
      コネクションプールに残ったシングルトン perform_query のせいでリトライ用の失敗注入が機能しなくなる。
    • ARCONN=trilogy + 特定シード (6, 7, 8, 10 など) でのテスト実行で再現していた。
  • このPR適用後は、上記の最小再現ケースおよび該当テストファイル全体が、シードに依存せず通ることが確認されています。

#58625 Avoid a redundant join in scoped through associations

マージ日: 2026/9/1 | 作成者: @dpaluy

  1. 概要 (1-2文で)
    has_many :through の関連にスコープをマージする際、すでに through の結合(JOIN)で登場しているテーブルがスコープ側でも再度 JOIN されてしまうケースで、その冗長な JOIN を自動的に削除するようにした変更です。belongs_to / has_one 方向の単一レコード JOIN に限って重複を取り除き、クエリ結果が変わり得るコレクション JOIN はそのまま残すようになっています。

  1. 変更内容の詳細

問題の背景

has_many :through に対して、スコープ内でさらに関連を joins していると、through のチェーンが貼る JOIN とスコープ側の JOIN が同じテーブルを重ねて JOIN してしまうことがありました。

例として PR 説明に出ているケース(#51259由来)を簡略化すると:

ruby
class Rubygem < ApplicationRecord
  has_many :versions
  has_many :dependencies, through: :versions

  # このあたりで versions / dependencies を join するスコープを
  # incoming_dependencies 的な名前で定義しているイメージ
end

has_many :through の定義と、そのスコープの中身の両方で versions を JOIN してしまうと、生成される SQL に同じテーブルが2回出てきます。

PR前のSQL:

sql
SELECT "rubygems".* FROM "rubygems"
  INNER JOIN "versions" ON "rubygems"."id" = "versions"."rubygem_id"
  INNER JOIN "dependencies" ON "versions"."id" = "dependencies"."version_id"
  INNER JOIN "versions" "versions_dependencies" ON "versions_dependencies"."id" = "dependencies"."version_id"
  WHERE "dependencies"."rubygem_id" = ?
    AND "versions"."indexed" = ?
    AND "versions"."position" = ?

3つめの INNER JOIN "versions" "versions_dependencies" は、実質的に同じ結合条件をもう一度書いているだけで、結果セットを絞り込みも増やしもしていない「無駄な JOIN」です。

このPRの対応内容

activerecord/lib/active_record/associations/association_scope.rbAssociationScope#add_constraints が変更され、以下のロジックが追加されています(要約):

  • has_many :through の関連を解決する際に:
    • すでに through チェーンの中で JOIN 済みのテーブルに対して、
    • マージ対象のスコープ側でも joins が指定されている場合、
    • その関連が belongs_to または has_one(= 単一レコードを返す関連)の場合には、
      • スコープ側の JOIN を「マージ前に」落としてしまう(削除する)。
  • 一方、has_many など「コレクションを返す JOIN」は削除しない。
    理由: JOIN を1つ減らすと行数が変わる可能性があるため(結合の多重度に影響する)。

結果として、上記の SQL は次のように簡潔になります。

PR後:

sql
SELECT "rubygems".* FROM "rubygems"
  INNER JOIN "versions" ON "rubygems"."id" = "versions"."rubygem_id"
  INNER JOIN "dependencies" ON "versions"."id" = "dependencies"."version_id"
  WHERE "dependencies"."rubygem_id" = ?
    AND "versions"."indexed" = ?
    AND "versions"."position" = ?

削除された versions_dependencies JOIN は、同じキーで同じテーブルを JOIN しているだけなので、行をフィルタしておらず、削除しても返るレコードは変わりません。

テスト・仕様面

  • activerecord/test/cases/associations/has_many_through_associations_test.rb にテストが追加され、#51259 のレポータが示したテストケースが通ることが確認されています。
    • 単純に結果が同じなだけでなく、「AST(Arelツリー)が、余計な JOIN を書かずに同じ関連を表現した場合と一致する」ことも検証しています。
  • activerecord/CHANGELOG.md に動作変更として記載されています。

なぜアプリ側で JOIN を消せないのか

PR説明にもある通り:

Applications cannot just drop the joins themselves: incoming_dependencies needs it for its own where, and raises without it. It is only redundant once the chain supplies the same table.

つまり:

  • スコープ単体で見ると、その joinswhere 句内で使っているため必須(消すと ActiveRecord::StatementInvalid などになる)。
  • しかし「has_many :through のチェーン経由で同じテーブルが JOIN される場合」に限って、その JOIN は結果として冗長になる。
  • この「冗長になるかどうか」の判定は、Active Record が内部の関連解決時にしか把握できないため、アプリケーションコード側で安全に消すのは難しい。

そこで、AssociationScope の中で「through チェーンによる JOIN 情報」と「スコープの JOIN 情報」を突き合わせ、冗長な単一レコードJOINだけを安全に落とすようにしている、という設計です。


  1. 影響範囲・注意点
  • 対象:
    • has_many :through 絡みで、関連のスコープ内にも同じテーブルへの belongs_to / has_one JOIN を記述しているコード。
    • また、そのスコープを has_many :through から利用しているケース。
  • 期待される影響:
    • 発行されるSQLのJOIN数が減り、無駄なJOINがなくなる。
    • 結果セット(返るレコード)は変わらない想定。
      なぜなら、削除対象は「同じキーで同じテーブルを結合している単一関連JOIN」であり、削除しても条件が変わらないから。
  • 意図的に「行数を変えうる」コレクションJOIN(has_many 側)は削除しないため、includes や手動で行っている複雑なコレクションJOINには影響を与えない設計です。
  • カスタムArelや、JOINエイリアス名に強く依存した高度なSQLハックをしている場合:
    • 内部的に冗長JOINが減ることで、結果的に「エイリアス名が減る」などの副作用が出る可能性はありますが、通常のActive Record APIの範囲では問題は起こりにくいと考えられます。

  1. 参考情報 (あれば)
  • このPRの元Issue: #51259
    • has_many :through のスコープが冗長なJOINを生む問題の報告と再現ケース。
  • 変更ファイル:
    • activerecord/lib/active_record/associations/association_scope.rb
      → JOINマージ時のロジックが追加。
    • activerecord/test/cases/associations/has_many_through_associations_test.rb
      → 冗長JOINが取り除かれること、およびASTが同一になることを確認するテスト。
    • activerecord/CHANGELOG.md
      → 振る舞い変更としての記載。

#58622 Avoid preloading nil converted association keys

マージ日: 2026/9/1 | 作成者: @ThrwatElmo

  1. 概要 (1-2文で)
    Rails の Active Record プリロード処理で、関連の外部キーが nil の場合に余計なクエリが発行されていた問題を修正し、nil キーを正しくスキップするようにした PR です。外部キー/主キーの型が異なり、プリローダがキーを文字列化するケースでも、nil は空文字列に変換せず nil のまま保持します。

  1. 変更内容の詳細

問題の背景

  • Active Record のプリローダ (ActiveRecord::Associations::Preloader) は、関連付けの外部キーと主キーの型が異なるとき、両方のキーを文字列化してマッチングします。
  • その際、オーナー側のキーが nil だった場合も文字列化され、""(空文字列)になっていました。
  • Ruby では空文字列は truthy なので、owners_by_key(キーごとにオーナーをまとめたハッシュ)に「キー: ""」として登録されてしまいます。
  • その結果、実際には関連キーが存在しない(nil)レコードに対してもプリロード用クエリが発行されるが、クエリの結果は当然ヒットせず、関連は最終的に nil になるだけ、という「無駄なクエリ」が発生していました。

今回の修正内容

Preloader::Association#convert_key の挙動を変更:

  • 変更前(概念的なイメージ):

    ruby
    def convert_key(key)
      key.to_s  # nil => ""
    end
  • 変更後(概念的なイメージ):

    ruby
    def convert_key(key)
      return nil if key.nil?
      key.to_s
    end

要点:

  • キー変換が必要な場合でも nilnil のまま残す
  • nil 以外のキーは従来通り to_s で文字列化する。

これにより、既存のガードロジック:

ruby
owners_by_key[key] # などで「キーが存在するか」を見ている処理

において、nil キーを持つオーナーは正しく除外されるようになります(型変換が不要なケースではもともとそうなっていた挙動と揃う)。

テストと関連調整

  • 既存の「文字列の外部キーが整数の主キーを指す」関連を用いたリグレッションテストを追加し、
    • nil の外部キーを持つレコードについて余計なクエリが走らないことを検証。
  • 既存の「クエリのグルーピング(条件の異なる 2 クエリになること)」をテストしていたケースは、今までは暗黙的に nil キー由来の不要なクエリに依存していたため、
    • 明示的に外部キーを設定して「2 種類の scope のクエリが発行される」ことをテストするように修正。

また、CHANGELOG にバグ修正として追記されています。


  1. 影響範囲・注意点
  • 対象: Active Record の関連プリロード(includes, preload, eager_load 等)で、
    • 外部キーと主キーの型が異なり、
    • プリロー ダ側でキー変換(文字列化)が行われるケース。
  • 影響:
    • これまで「外部キーが nil なのにプリロードクエリが飛んでいた」パターンで、余計なクエリが出なくなるため、若干のパフォーマンス改善・無駄な DB 負荷軽減が見込めます。
    • 検索結果そのもの(関連が nil になる/ならない)はもともと正しかったため、アプリケーションの表向きの挙動は基本的に変わりません
  • 互換性:
    • アプリ側で「なぜか 1 本余計なクエリが走っている」ことに依存していたようなコードがない限り、互換性問題はほぼありません。
    • 監視やテストで「発行クエリ数」を厳密に見ている場合は、関連のプリロードでクエリ数が減ることによりテストが変化する可能性があります(N+1 検出用のメトリクスなど)。

  1. 参考情報 (あれば)
  • 対応ファイル:
    • activerecord/lib/active_record/associations/preloader/association.rb
    • activerecord/test/cases/associations_test.rb
    • activerecord/CHANGELOG.md
  • テスト状況:
    • ARCONN=sqlite3 bin/test test/cases/associations_test.rb
      • 137 runs, 525 assertions, 0 failures, 0 errors, 0 skips
  • RuboCop チェックも問題なしとのこと。

#58626 Optimize EventReporter::LogSubscriber#subscription_filter

マージ日: 2026/9/1 | 作成者: @byroot

  1. 概要 (1-2文で)
    EventReporter::LogSubscriber#subscription_filter の実装を見直し、イベント名から namespace を抽出するのではなく、namespace から prefix を組み立てる形に変更することで、処理を単純化・最適化した PR です。挙動は従来と同等で、主に内部実装の整理・パフォーマンス改善が目的です。

  1. 変更内容の詳細

※ 実際の diff は 2 行追加・6 行削除と小規模ですが、概要説明に基づくとロジックの方向性が変わっています。

従来:

  • 「イベント名("render.action_view" のような文字列)から namespace を切り出す」処理をしていた。
  • 例えば event.name.split(".").last のような形で "action_view" 部分を取り出してフィルタに使っていた、というイメージです。

今回の変更:

  • 「namespace(action_view など)から prefix("action_view." のような文字列)を組み立てる」アプローチに変更。
  • イベント名 → namespace の抽出処理をなくし、namespace を元に subscription 用の prefix を作ることで、不要な文字列処理を削減しています。

イメージ的なコード変化は以下のようなものです(擬似例・イメージであり実際のコードではありません):

ruby
# 変更前(イメージ)
def subscription_filter(event)
  # event.name: "render.action_view"
  namespace = event.name.split(".").last   # => "action_view"
  # namespace を元に何らかの条件判定
  namespace == expected_namespace
end

# 変更後(イメージ)
def subscription_filter(event)
  # 事前に namespace から prefix を作る
  prefix = "#{expected_namespace}."
  # event.name: "render.action_view"
  event.name.start_with?(prefix)
end

ポイント:

  • 「イベント名から namespace を抽出する」ための split などの処理が不要になる。
  • フィルタ条件として単純な start_with? のような prefix マッチが使える。
  • これにより、処理コストとコードの複雑さが下がる。

EventReporter::LogSubscriber は ActiveSupport のイベントレポート機能に紐づいたログ購読処理なので、この最適化はイベント購読フィルタが多く呼ばれるケースでじわっと効く可能性があります。


  1. 影響範囲・注意点
  • 外部 API の挙動は原則として変わらない
    PR の説明からは、あくまで内部実装の最適化であり、EventReporter::LogSubscriber の public API や利用方法を変えるものではありません。

  • イベント名の構造前提は維持されている
    namespace から prefix を構築する」ということは、イベント名が "something.namespace" のような形式になっている前提が引き続きあります。
    既存の ActiveSupport インストゥルメンテーションの命名規則に乗っていれば問題はありません。

  • 独自拡張している場合は注意

    • EventReporter::LogSubscriber#subscription_filter を直接 monkey patch している、
    • あるいは EventReporter 周りの内部実装に強く依存したコードを書いている
      といったケースでは、この変更を取り込んだ際に挙動を一度確認することをおすすめします。
  • パフォーマンス面のわずかな改善が期待される
    特に大量のイベントが流れる本番環境などで、文字列 split / 解析が減る分、オーバーヘッドが僅かに減ります。ただし変更規模からして「劇的な改善」ではなく、地味な最適化レベルです。


  1. 参考情報 (あれば)

この PR はその LogSubscriber の一種である EventReporter 用実装の内部ロジックを整理・最適化するもので、Rails アプリケーションコード側の修正は基本的に不要です。


#58303 Require concurrent before using in ActionCable

マージ日: 2026/9/1 | 作成者: @josevalim

  1. 概要 (1–2文で)
    Action Cable を単体で読み込んだときにクラッシュしていた問題を、必要な concurrent 系モジュールを明示的に require することで解消した PRです。Action Cable を Rails 本体経由ではなく、直接ロードして使うケースでの初期化エラーを防ぐためのバグフィックスです。

  1. 変更内容の詳細

何が問題だったか

PR の説明にある再現コード:

bash
ruby -Iactivesupport/lib -Iactioncable/lib \
  -e 'require "action_cable"; ActionCable.server.executor'

のように、Rails 全体ではなく activesupportactioncable だけを -I で指定して require "action_cable" した場合に、ActionCable.server.executor の呼び出しでクラッシュしていました。

原因は、Action Cable 内部で concurrent-rubyActiveSupport::Concurrency / Concurrent::Map など)に依存しているにもかかわらず、Action Cable 単体ロード時にはその依存を自前で require していなかったことです。Rails アプリ全体を require "rails/all" 等で読み込む場合は、他のコンポーネントが concurrent を読み込むため問題が表面化しませんでした。

実際の変更点

変更ファイルは 2 つで、それぞれ 1 行の require が追加されています。

  • actioncable/lib/action_cable/channel/base.rb
  • actioncable/lib/action_cable/server/base.rb

PR タイトルの「Require concurrent before using in ActionCable」から推測される通り、これらのクラスが concurrent ベースの機能(スレッドセーフなデータ構造や executor 等)を使う前に、明示的に:

ruby
require "active_support/concurrent" # もしくはそれに準ずる concurrent 関連ファイル

のような行が追加されています。

イメージとしては以下のような差分です(実際のファイル名やパスは PR 依存ですが、概念的にはこのような追加です):

ruby
# actioncable/lib/action_cable/server/base.rb

# 追加された行
require "active_support/concurrent"

module ActionCable
  module Server
    class Base
      # ここで executor などの concurrent な仕組みを利用
    end
  end
end

同様に、ActionCable::Channel::Base でも concurrent な構造(例: connection の管理や subscription の状態保持など)を使う前に require を追加しています。

これにより、Rails 以外の環境や、ミニマルなロードパスで Action Cable を利用した際にも、必要な concurrent 機能が確実に読み込まれるようになります。


  1. 影響範囲・注意点
  • 影響範囲
    • 主に「Action Cable をライブラリとして単体利用する」「最小限の gem ロードで動かす」ような環境に影響します。
      • 例: require "action_cable" だけをして WebSocket サーバ機能を組み込むようなユースケース
    • Rails フルスタック(rails new で生成された通常のアプリ)では、元々別コンポーネント経由で concurrent が読み込まれていたため、挙動はほぼ変わりません。
  • 互換性
    • 既存機能の挙動を変えるものではなく、「今までクラッシュしていたパスが動くようになる」方向の変更です。
    • 追加されたのは require 行のみであり、API 変更や挙動変更はありません。
  • パフォーマンス
    • concurrent-ruby 関連の require が Action Cable ロード時に確実に走るようになるため、Action Cable 初回ロード時のオーバーヘッドがごくわずかに増えますが、実用上は無視できるレベルです。

  1. 参考情報 (あれば)
  • PR 番号: https://github.com/rails/rails/pull/58303
  • 類似の問題は、「コンポーネント単体ロード時に他コンポーネントへの暗黙の依存が崩れる」パターンで発生しがちです。Rails コンポーネントを単体 gem として利用する場合は、「自分のコンポーネント内で使っている外部モジュールは明示的に require する」ことを意識しておくと同種のバグを防げます。

#58620 Make some ActionView::Base settings shareable

マージ日: 2026/8/31 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::Base が持ついくつかの設定値を Ractor 間で共有可能(shareable)にし、Ractor を使ったビュー描画が Ruby 4.0 でも問題なく行えるようにする変更です。クラス変数ベースの cattr_accessor を、シングルトンクラス上の attr_accessor に置き換えることで、Ractor の shareable 制約に対応しています。

  1. 変更内容の詳細

主な目的

  • Ractor 上で ActionView::Base を使ってビューをレンダリングできるようにするため、ActionView::Base の「設定値」を Ractor-shareable な形で保持する。
  • 具体的には、以下の設定を Ractor から安全に読める/セットできるようにする:
    • automatically_disable_submit_tag
    • remove_hidden_field_autocomplete
    • default_formats
    • field_error_proc

実現方法

1) cattr_accessor からシングルトンの attr_accessor への変更

今まで:

ruby
class ActionView::Base
  cattr_accessor :automatically_disable_submit_tag, default: true
  cattr_accessor :remove_hidden_field_autocomplete, default: true
  cattr_accessor :default_formats
  cattr_accessor :field_error_proc
end

のように、クラス変数 (@@var) を内部的に使う cattr_accessor を利用していた部分を、

ruby
class ActionView::Base
  class << self
    attr_accessor :automatically_disable_submit_tag,
                  :remove_hidden_field_autocomplete,
                  :default_formats,
                  :field_error_proc
  end
end

のように、シングルトンクラスのインスタンス変数 (@var) を使う attr_accessor に変更しています。

ポイント:

  • cattr_accessor が定義していた「インスタンスメソッド側のアクセサ」は使われていない、という前提に立っているため、この変更は既存の呼び出し(クラスメソッドとしての呼び出し)には影響しない。
  • Ractor の shareable 制約的に、クラス変数よりもクラスインスタンス変数の方が扱いやすく、今回の実装方針に合致する。

2) 各設定値ごとの shareable 性の扱い

  • automatically_disable_submit_tag
  • remove_hidden_field_autocomplete

これらは bool 値 (true/false) で運用されており、そのままでも shareable。
※ 型をより厳密に true/false に縛ることも検討できるが、現状はそこまでしていない、という位置づけ。

  • default_formats

    • ブート時にミュータブルに設定されるが、Mime.eager_load! によって shareable な状態になるよう保証される。
    • Rails 起動後は、Ractor 間で読み取り可能な不変(or 実質不変)の配列/構造として扱える前提。
  • field_error_proc

    • Proc オブジェクトであり、デフォルト値および設定時に shareable にしておく必要がある。
    • この PR では、ロード時に設定されるデフォルトの field_error_proc を shareable な形で保持し、さらにユーザーが値を上書きする際も shareable であることを満たすようにしています(テストでその前提を検証)。

3) Ractor 向けのテスト追加

actionview/test/template/base_ractor_test.rb が新規追加・拡張され、以下を検証:

  • Ractor 内で ActionView::Base を使ったレンダリングが行えること。
  • Ractor 内から automatically_disable_submit_tag, remove_hidden_field_autocomplete, default_formats, field_error_proc にアクセスしてもエラーにならないこと。
  • field_error_proc を Ractor 上で評価しても動作すること(shareable な Proc であることの確認)。

全体として、「Ractor 上から ActionView::Base の設定値にアクセスしても、shareable 制約に引っかからず動作する」ことを CI テストで保証しています。


  1. 影響範囲・注意点
  • 既存 API との互換性

    • 公開インターフェースとしてはクラスメソッド (ActionView::Base.default_formats など) のままなので、基本的にアプリケーションコードに対する互換性は高いです。
    • ただし、もし内部実装に依存して「クラス変数 @@foo を直接読む/書く」ようなメタプログラミングをしている場合は壊れます(@@ ではなくクラスインスタンス変数 @ に変更されるため)。
  • Ractor を使わない場合

    • Ractor を使わない通常の Rails アプリでは、挙動上の違いはほぼありません。
    • cattr_accessor が生成していたインスタンスメソッド (view = ActionView::Base.new; view.default_formats のような呼び出し) がもし暗黙に使われていた場合は注意が必要ですが、PR 説明によるとこのインスタンス側アクセサは使われていない前提で変更されています。
  • Ractor を使う場合

    • field_error_proc をカスタマイズする場合、その Proc が Ractor shareable である必要があります。
      • キャプチャしている外部変数や、閉じているオブジェクトが Ractor shareable でないとエラーになる可能性があります。
      • シンプルなラムダ/Proc、凍結されたオブジェクトのみを参照する Proc にしておくのが安全です。
    • default_formats に対し、ブート後にミュータブルな変更(破壊的な <<, map! など)を行うと、Ractor 間共有との整合性が崩れる可能性があるため、基本的には「起動時に確定させ、その後は不変として扱う」想定です。
  • Ruby バージョンとの関係

    • Ruby の「次のバージョン」ではクラス変数からの shareable 値の読み取りサポートが入る予定だが、この PR により「Ruby 4.0 でも問題なく動く」よう、先行してクラスインスタンス変数+Ractor shareable 設計にしている、という位置づけです。

  1. 参考情報 (あれば)
  • Ractors と shareable オブジェクトについて(Ruby 3 以降の仕様)
    • Ractor 内で共有できるのは「shareable」なオブジェクトに限られ、基本的に:
      • 不変オブジェクト(凍結された文字列、数値、シンボルなど)
      • 特定のルールで構成された凍結済みの配列・ハッシュなど
      • shareable な Proc(Env が shareable)
    • それ以外は Ractor 間に送れない/参照できない。
  • この PR: rails/rails #58620 「Make some ActionView::Base settings shareable」
    (ActionView で Ractor 対応を進めるための一環と見なせます)

#58606 Fix encrypted fixtures for JSON columns

マージ日: 2026/8/31 | 作成者: @carlosdanielpohlod

  1. 概要 (1-2文で)
    config.active_record.encryption.encrypt_fixtures = true を有効にした状態で、json / jsonb カラムを encrypts している属性のフィクスチャ読込時に発生していた復号エラー(#48601)が、暗号化フィクスチャのシリアライズ処理を修正することで解消されています。暗号化済みペイロードが JSON カラム用に二重シリアライズされていたのを防ぐパッチです。

  1. 変更内容の詳細

問題の背景

  • 前提:

    • config.active_record.encryption.encrypt_fixtures = true
    • モデルで encrypts :some_attr を宣言
    • some_attr が DB 上では json / jsonb カラムであるケース
  • これまでの挙動:

    1. EncryptedFixtures がフィクスチャ値を暗号化し、「暗号化済み文字列」をフィクスチャ値として扱う。
    2. フィクスチャを DB に流し込む build_fixture_sql が、全カラム値に対して column.type に応じた型シリアライズ (column.cast_type.serialize) を行う。
    3. json / jsonb カラムの場合:
      • Type::Json が値を JSON 文字列にエンコードするため、すでに暗号化済みの文字列がさらに JSON 文字列として包まれる。
      • 結果として DB には "{\"p\":...}" のような「JSON 文字列としての暗号文」が保存される。
    4. レコード読込時、Active Record の復号処理は「JSON オブジェクトの中身」を期待しているのに、実際には「JSON カラムの中に入ったただの文字列」が渡されるため、ActiveRecord::Encryption::Errors::Decryption が発生していた。
  • 文字列カラム (string) はどうだったか:

    • Type::String のシリアライズは「文字列をそのまま返す(恒等写像)」なので、暗号化済み文字列をさらに壊さず、そのまま保存されて問題が表面化しなかった。

今回の修正方針

EncryptedFixtures now hands the payload back in the column's cast form (column.cast_type.deserialize), so build_fixture_sql serializes it once and stores the same JSON object a regular save would.

ポイントは「EncryptedFixtures がフィクスチャ値として返すオブジェクトの型」を、各カラム型の「キャスト済みオブジェクト」に揃えたことです。

  • 変更前(概念的な流れ):

    • EncryptedFixtures:
      • フィクスチャの「論理値」 → 暗号化 → 「暗号化済み文字列」をそのまま返す
    • build_fixture_sql:
      • 受け取った文字列を column.cast_type.serialize に通す
      • json カラムなら JSON 文字列化され、結果として二重ラップされる
  • 変更後(概念的な流れ):

    1. EncryptedFixtures が暗号化ペイロードを、そのカラムの cast_type に対する deserialize 済み の形にして返す:
      • column.cast_type.deserialize(encrypted_payload)
    2. build_fixture_sql は、いつも通り column.cast_type.serialize を一度だけ呼ぶ。
    3. その結果、json / jsonb カラムでは、通常の save と同じ JSON オブジェクト構造が DB に保存されるようになる。

ここでのキモは:

  • フィクスチャ → 暗号化 → column.cast_type.deserializebuild_fixture_sqlserialize
  • という 「deserialize → serialize の 1 往復」 によって、「通常のモデル保存と同じ型変換ルート」を通るようにしたことです。

コードレベルの変更点(概要)

activerecord/lib/active_record/encryption/encrypted_fixtures.rb

  • 暗号化済みペイロードを返す処理で、生の文字列を返さず、カラムの cast_type.deserialize を通した値を返すように変更。
  • これにより、フィクスチャローダ側は「既にカラム型としてキャスト済みの値」として扱える。

テスト関連:

  • activerecord/test/cases/encryption/encrypted_fixtures_test.rb
    • JSON カラムを encrypts したモデルのフィクスチャが、エラーなく読み書きでき、かつ期待どおりの JSON 形式で保存されることを確認するテストを追加。
  • activerecord/test/fixtures/encrypted_book_with_json.yml
    • 暗号化 JSON カラム用のフィクスチャを追加。
  • activerecord/test/models/book_encrypted.rb
    • JSON カラムを持ち encrypts を宣言したモデルを追加。
  • activerecord/test/schema/schema.rb
    • 上記モデル用に JSON カラムをスキーマに追加。

  1. 影響範囲・注意点
  • 影響を受けるケース:

    • Active Record Encryption を使用し、
    • config.active_record.encryption.encrypt_fixtures = true を有効にしており、
    • encrypts 対象の属性が DB 上で json / jsonb カラムになっている場合。
  • この PR による挙動の変化:

    • これまで:
      • 暗号化 JSON カラムのフィクスチャ読み込み時に ActiveRecord::Encryption::Errors::Decryption が発生し得た。
      • DB 内のデータ形式が "\"{...}\"" のような形になる可能性があった。
    • これから:
      • 復号エラーが解消される。
      • DB に保存されるデータ形式が「通常の保存時」と同じ JSON 構造になる。
  • 既存データへの影響:

    • すでに「二重シリアライズされた状態」で DB に入っているデータは、この修正で自動変換されるわけではありません。
    • そうしたデータがあれば、以下のような対応を検討する必要があります:
      • データをダンプして再インポートする
      • 手動/スクリプトで値を正しい JSON 形式に変換する
    • ただし、これは「テスト用フィクスチャ」の利用が主なケースであり、本番 DB に影響することは少ないはずです。
  • 文字列 / バイナリカラム:

    • PR の説明にもある通り、string / binary カラムは「serialize / deserialize が恒等変換」であるため、挙動は従来と変わりません。
  • カスタム型:

    • json / jsonb 以外にも、独自の cast_type を定義しているカラムがある場合、EncryptedFixtures は「そのカスタム型の deserialize → serialize」のラウンドトリップを通るようになります。
    • 通常は「通常保存と同じ挙動になる」ので望ましい変更ですが、もし独自型がフィクスチャ処理を前提に特殊な実装をしている場合は挙動が変わる可能性があります(かなりレアケース)。

  1. 参考情報 (あれば)
  • 関連 Issue:

    • #48601 – JSON / JSONB カラム + encrypt_fixtures = true での復号エラー報告
  • 関連設定:

    • config.active_record.encryption.encrypt_fixtures = true
      • テストフィクスチャをロードする際に、暗号化対象属性を自動で暗号化してから DB に保存するための設定。
  • 実務的な確認ポイント:

    • 暗号化 JSON カラムを使っている場合、Rails をこの修正を含むバージョンに上げた後、次を確認すると安全です:
      • フィクスチャロード(rails test, rails db:fixtures:load 等)がエラーなく完走すること
      • DB に保存されている JSON カラムのフォーマットが期待どおりであること("{\"p\":...}" のような「さらにクォートされた文字列」になっていないこと)

#58612 Fix NameError when ActiveRecord::Coders::JSON is given encode options

マージ日: 2026/8/31 | 作成者: @lazerg

  1. 概要 (1-2文で)
    ActiveRecord の ActiveRecord::Coders::JSONencode_options を渡したときに NameError が発生していた回 regresson を修正した PRです。イニシャライザ引数名のリネーム漏れによるバグを直し、encode オプション付きの利用を担保するテストが追加されています。

  1. 変更内容の詳細

バグの内容

以下のようなコードを書くと:

ruby
coder = ActiveRecord::Coders::JSON.new(encode_options: { escape: true })

実行時に:

text
NameError: undefined local variable or method `options'

というエラーが発生していました。

原因は、以前の PR #58601 で ActiveRecord::Coders::JSON のイニシャライザ引数が

ruby
def initialize(options = {})

のような形から、キーワード引数 encode_options: に変更された一方で、内部でデフォルト値とマージするコードがまだ options という古い変数名を参照していたためです。

イメージとしては、元がこんな感じだったのに対し:

ruby
def initialize(encode_options: {})
  @encode_options = DEFAULT_ENCODE_OPTIONS.merge(options) # ← ここが古い名前
end

options というローカル変数が存在しないため NameError になっていました。

修正内容

この PR では、そのマージ処理を正しく encode_options に合わせるように 1 行だけ修正しています。

ruby
@encode_options = DEFAULT_ENCODE_OPTIONS.merge(encode_options)

これにより、以下のような「デフォルトの JSON エンコードオプションに対して、呼び出し側で上書きする」という本来の挙動が復活します。

ruby
coder = ActiveRecord::Coders::JSON.new(
  encode_options: { escape: true }
)
# DEFAULT_ENCODE_OPTIONS をベースに { escape: true } が上書きされる

テストの追加

これまでのテストは:

  • デフォルトのエンコードオプションでの動作
  • デコード処理側

のみをカバーしており、「encode_options を利用して JSON コーダを生成するケース」がテストされていませんでした。

そこで、この PR では activerecord/test/cases/coders/json_test.rb に、新たに encode_options を指定した場合のテストが追加されています。内容としてはおおむね:

  • ActiveRecord::Coders::JSON.new(encode_options: {...}) が例外を出さずに初期化できること
  • そのオプションが内部に反映されていること

を確認するものです。


  1. 影響範囲・注意点
  • 対象ブランチ: main および 8-1-stable にのみ存在する regression であり、まだリリース版には入っていないため、現時点の正式リリースを使っているアプリには影響しません。
  • 影響範囲:
    • ActiveRecord::Coders::JSON直接 使い、かつ encode_options: キーワード引数を渡しているコードが対象です。
    • Rails 内部でこのクラスを使っている箇所については、encode_options を明示的に指定していなければ影響はありません。
  • 挙動の変化:
    • 既存の挙動(デフォルトオプション+呼び出し側オプションのマージ)が本来のとおりに復活するだけで、API 仕様の変更や互換性破壊はありません。
  • 注意点:
    • まだ未リリースのため CHANGELOG には記載されていません。main/8-1-stable を追いかけている場合は、この PR を取り込むか、該当期間のコミットで回避されているか確認する必要があります。
    • 自前で monkey patch している場合は、この修正と二重にパッチを当てていないか確認してください。

  1. 参考情報 (あれば)
  • 該当 Issue: #58609 (ActiveRecord::Coders::JSON.new(encode_options: ...) での NameError 報告)
  • 関連 PR: #58601 (optionsencode_options へのイニシャライザ変更を行った PR。今回の regression の原因)