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-2文で)
このPRは、Rails内部で使われていたクラス変数ベースの設定(mattr_accessor)をインスタンス用のattr_accessor等に置き換え、Ruby 4.0.6 でも Ractor から安全に参照できるようにするリファクタリングです。主に Action Pack / Action View の各種ヘルパの設定値の持ち方が変わり、それに対応するテストが追加されています。
- 変更内容の詳細
背景
- 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関連設定)を保持するクラスレベルの状態管理から、インスタンスアクセサ経由の管理に移行。もともと以下のような形式だったと考えられます(疑似コード):
rubyclass 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.rbactionview/lib/action_view/helpers/form_helper.rbactionview/lib/action_view/helpers/form_tag_helper.rbactionview/lib/action_view/helpers/navigation_helper.rb
ここでは、以下のような「グローバルなヘルパ設定」を担っていそうなフィールドが対象になっています。
例(代表的なパターンのイメージ・疑似コード):
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/FormTagHelperdefault_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 対応テストのベースクラス/共通化のための試験コードが追加されている可能性が高いです。
- 影響範囲・注意点
公開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の設計上、そのような継承前提の使い方を避ける方向に寄せる意味でも、この変更は理にかなっていますが、もし独自にヘルパを継承していて内部変数を見ている場合は挙動確認をした方が安全です。
- 参考情報 (あれば)
- 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-2文で)
RailsアプリケーションをRactor対応(並列実行)した際に、「ビューのテンプレート探索(view paths lookup)」ができない問題を修正し、Ractor内からもビューのパスにアクセスできるようにした変更です。具体的には、Ractorで共有できなかったビュー関連キャッシュを「Ractor-shareable」になるように扱う(freezeする)ことで、Ractor内からのテンプレート検索を可能にしています。
- 変更内容の詳細
背景・問題点
- Ractor内でテンプレートをレンダリングしようとすると、テンプレート探索処理が内部で参照するビュー関連のキャッシュが「Ractor-shareable」ではないためエラーになる、もしくはアクセスできませんでした。
- Railsのビューシステム(Action View)は、テンプレート探索結果などをキャッシュして高速化していますが、このキャッシュオブジェクトがミュータブルな状態だとRactor間で共有できません。
- Ractorでは「凍結された(freezeされた)オブジェクト」など、一部の条件を満たすオブジェクトだけが共有可能です。
このPRでの方針
- 「Ractor化(ractorize)されたアプリケーション」では、ビュー関連のキャッシュを凍結してRactor-shareableにする、という割り切った実装を採用しています。
- これは「eager load が有効な環境」を前提にしています。理由は、lazy load/autoload 中にコントローラが
append_view_pathsなどでキャッシュを書き換える可能性があるためです。
PRの説明中の例:
class MyController < ApplicationController
append_view_paths("some/folder") # ここでビューのパスに関するキャッシュが更新される
endこのようなコードが autoload のタイミングで実行されると、Ractor化した後にキャッシュがミュータブルなまま書き換えられてしまう可能性があります。そのため、
- 「eager load を完了させてから」
- 「ビュー関連のキャッシュを freeze して」
Ractor実行を開始する、というフローを前提にしています。
具体的なコード変更(ファイルごとの概要)
※行数から読み取れる範囲での性質・目的を説明します。
actionview/lib/action_view/path_registry.rb(+11/-0)
- View paths のレジストリ(
ActionView::PathRegistry)周りに、Ractor対応のための変更が入っています。 - 典型的には以下のようなことをしている可能性が高いです:
- 内部のキャッシュ構造(たとえば
@cacheのようなハッシュや配列)を凍結するメソッドの追加 - Ractor対応用のメソッド(例:
ractorize!など)で、パスレジストリ内のオブジェクトを再構成/コピーして shareable にする、あるいはfreezeする
- 内部のキャッシュ構造(たとえば
- これにより、
ActionViewが持つビュー探索キャッシュが Ractor からも参照可能な形になります。
railties/lib/rails/application.rb(+1/-7)
- Railsアプリケーション全体をRactor対応するための「ractorize」処理の中で、ビュー関連キャッシュの扱いを変更しています。
- 行数減から推測すると:
- 以前は ractorize の中で別の(より大きな)処理をしていたのを簡略化し、View Path Registry 側の新しいAPI(freeze処理など)を呼び出すように整理した可能性があります。
- もしくは、複数箇所でやっていたキャッシュの調整を一本化した、または eager load 前後のタイミングを整えた変更です。
railties/test/application/ractors_test.rb(+16/-0)
- Ractor内からビューにアクセスできることを検証するテストが追加されています。
- 典型的には次のような流れのテストが考えられます:
- テスト用のアプリケーションを eager load する
- アプリケーションを Ractor 用に初期化(ractorize)する
- Ractor を起動し、その中でコントローラまたはビューをレンダリングするコードを実行
- エラーなくテンプレートが見つかりレンダリングできるかを確認
このテストにより、「Ractor内でのテンプレート探索が実際に成功すること」が自動的に保証されるようになっています。
- 影響範囲・注意点
影響範囲
- 影響するのは「Ractor を使って Rails アプリケーションを実行し、Ractor 内でビューをレンダリングするケース」です。
- 通常の(シングルRactor、マルチスレッド)運用のみであれば振る舞いはほぼ変わらない想定です。
- Action View のパスレジストリやビュー探索キャッシュの内部実装に依存しているコードがあれば、freeze による影響(変更不能になること)を受ける可能性はありますが、普通のアプリケーションでは直接触ることはまれです。
注意点
eager load が前提
- Ractorでビューを使いたい場合、
config.eager_load = trueなどにして、アプリケーションとビュー関連クラスがあらかじめ eager load されている必要があります。 - 開発環境のように autoload / lazy load に頼っている設定では、
- コントローラが autoload されるタイミングで
append_view_pathsなどを実行し、 - それが freeze 後のキャッシュに対して変更を試みる
といった問題が起きる可能性があります(これはPR中でも「非 eager loaded 環境はまだ最適化の対象外」と明言)。
- コントローラが autoload されるタイミングで
- Ractorでビューを使いたい場合、
append_view_pathsなどの動的変更との相性- Ractor化後(キャッシュ freeze 後)に
append_view_pathsのような API を使うと、- 例外になる
- あるいは無視される
など、挙動に制約が出る可能性があります。
- Ractor運用を想定するなら、ビューのパス構成は起動時に確定させ、実行時に追加/変更しない設計が望ましいです。
- Ractor化後(キャッシュ freeze 後)に
Ractor対応はまだ実験的な領域
- PR本文からも、「まずは eager load 環境に絞って現実的な解決を入れた」というスタンスが読み取れます。
- 非 eager load 環境でRactorを使うと、今回のビューキャッシュ以外にも同様の問題(ミュータブルなグローバル状態)が多数あると考えられるため、Ractorを本番導入する場合は十分な検証が必要です。
- 参考情報 (あれば)
- 関連しそうな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設計に沿った妥当なものです。
- Ruby 3系のRactorでは、
- 今後の展望:
- 非 eager load / 開発環境でのRactorサポートをきちんと行おうとすると、autoloader(Zeitwerk)や各種キャッシュ、グローバル設定を広範に見直す必要があります。
- このPRは「まずはRactorでもビューが使えるようにする」という第一歩的な位置付けで、Ractorサポート全体の中では一部機能の解消に相当します。
#58659 Fix Ractor unshareable procs at boot
マージ日: 2026/9/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails のブート時に生成されるいくつかの proc / lambda が Ractor 的に「共有不可能(unshareable)」になっていた問題を、キャプチャするローカル変数を整理することで「共有可能(shareable)」にする修正です。Ractor ワーカー利用時の IsolationError を避けつつ、ルートリローダーなど一部の「どうしても共有できない値」は Ractor 非対応機能として扱う方針を明示しています。
- 変更内容の詳細
背景: Ractor と shareable / unshareable オブジェクト
Ruby 3 の Ractor では、Ractor 間で自由に共有できるのは「shareable」にマークされたオブジェクトだけです。Proc / lambda も、そのクロージャがキャプチャしている値に「unshareable」なオブジェクト(例: ミューテックス、IO、通常のオブジェクトグラフなど)が含まれていると Ractor.make_shareable できず、Ractor ワーカー起動時などにエラーになります。
この PR では、
- Rails のブートプロセスで定義される複数の
lambda/procが - ブロック内で不要に「unshareable なコンテキスト」をキャプチャしてしまっていたのを
- 「必要なローカル変数だけを明示的に取り出してキャプチャする」パターンに統一
することで、Ractor 対応を強化しています。
主なパターン: 「使う値だけローカルに退避してから lambda を定義」
ほとんどの変更は同じリファクタリングパターンです。
典型パターン(抽象化)
修正前(イメージ):
some_setup do
# ここで self や外側のコンテキストをそのままキャプチャする
callback = -> { do_something_with(self.config) }
register_callback(callback)
end修正後(イメージ):
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.rbactivejob/lib/active_job/railtie.rbactiverecord/lib/active_record/associations/builder/association.rbactiverecord/lib/active_record/belongs_to.rbactiverecord/lib/active_record/secure_token.rbrailties/lib/rails/application/finisher.rbrailties/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 であるべきか」の仕様を事実上固定し、今後のリグレッションを防ぐ役割を担っています。
- 影響範囲・注意点
- 影響を受けるのは主に Ractor を使うアプリケーション
- Ractor ワーカーを利用していない通常の Rails アプリでは、挙動の変化はほぼありません(クロージャの書き方が変わっただけで、ロジックは同じ)。
- Ractor ワーカー + コードリロード有効 は依然として非サポート
- ルートリローダーは unshareable なままなので、Ractor ワーカーとコードリロードを同時に有効にする構成は、引き続きサポート外です。
- その場合、reloader に関わる処理が Ractor 内で実行されると
IsolationErrorが発生し得ます。
- 独自の Railtie / Engine / 初期化処理を書く場合の実務的注意
- Ractor を見据えている場合、今回の PR のパターンを真似するのが安全です:
initializerやto_prepareなどでlambda/procを定義するときは、selfやRails.applicationなどを丸ごとキャプチャしない- 必要な情報だけローカル変数に抜き出し、その変数だけをクロージャ内で参照する
- shareable でないオブジェクト(DB コネクション、ミドルウェアインスタンスなど)は Ractor 間で共有しない前提で設計する
- Ractor を見据えている場合、今回の PR のパターンを真似するのが安全です:
- 参考情報 (あれば)
- 該当 PR:
- 依存している PR / コミット:
- #58655
- コミット
88f486fc18
- Ractor / shareable について:
- Ruby 3.0 Ractor docs: https://docs.ruby-lang.org/en/master/doc/ractor_md.html
(Ractor.make_shareableや shareable object の制約など)
- Ruby 3.0 Ractor docs: https://docs.ruby-lang.org/en/master/doc/ractor_md.html
#58669 Fix broken link to Redirecting Requests in layouts_and_rendering.md
マージ日: 2026/9/4 | 作成者: @yahonda
- 概要 (1-2文で)
Rails ガイド「Layouts and Rendering」の中で、存在しないアンカー#redirecting-requestsへの自己リンクがあったため、正しいガイド「Action Controller Overview」の該当セクションへのリンクに修正した PR です。これによりrake guides:lintのリンクチェックが再び通るようになっています。
- 変更内容の詳細
問題点
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
- ローカルで以下のコマンドを実行し、いずれも exit code 0 で通ることを確認済み:
- 影響範囲・注意点
影響範囲
- 変更対象はガイド文書 (
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 箇所のみを修正)。 - 将来的にレイアウト/レンダリングのガイドに「リダイレクト」に関する独自セクションを追加する場合は、アンカー名の重複やリンク先の混在に注意する必要があります。
- もし他ガイドから
- 参考情報 (あれば)
- 問題を検知した
rake guides:lintの Buildkite ログ(PR 説明より) - 関連 PR
- #57152: Layouts and Rendering ガイドの書き直し(この変更が入って以降でリンク切れが発生)
- 修正対象ファイル
guides/source/layouts_and_rendering.md(+2 / -1, リンク 1 箇所の微修正)
#57152 [RF-DOCS] Rewrite the Layouts and Rendering guide
マージ日: 2026/9/4 | 作成者: @ayushn21
- 概要 (1-2文で)
Layouts and Rendering ガイドがほぼ全面的に書き直され、内容の重複を整理しつつ「コントローラとビューの関係」を中心とした構成に再編されました。これに伴い、レイアウト構造やパーシャルなどの説明は Action View Overview へ移動され、リクエストバリアントの説明は Action Controller Overview から本ガイドへ移されています。
- 変更内容の詳細
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 ガイドへ移動
(レンダリングの話とまとめることで、より自然な流れに)
- Request variants のセクション → Layouts and Rendering ガイドへ移動
- 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 :ok、head :no_content(204)、head :not_foundなど API エンドポイントや条件チェックに使う場面
- 例:
- 「なぜヘッダだけ返すのか?」という動機例を入れる方向
2.2.2 レンダリングの詳細 (render, redirect_to, head)
- 「Using
render」セクションを独立したトップレベルセクションに格上げする提案が反映されている可能性が高いです。- 同列に「Rendering by Default」「Using
redirect_to」「Usinghead」といったセクションが整理され、サイドバーに出るように構造改善。
- 同列に「Rendering by Default」「Using
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.erb、yield、content_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 へのリンク など、**「レンダリングへの入口」としての説明が手厚くなっていると考えられます。
- 行数が +1003 / -839 と大きく変化しており、
- 影響範囲・注意点
- コードへの直接の影響はなし(ガイドのみの変更)ですが、学習・参照パターンには影響があります。
- 以前: Layouts and Rendering ガイドに「パーシャル」「アセットタグ」「
yield/content_for」「ネストレイアウト」などが分散していた。 - 今後:
- コントローラが何を返すか/どうレンダするか → Layouts and Rendering
- ビュー/レイアウトの書き方・構造・ヘルパー → Action View Overview
- 以前: Layouts and Rendering ガイドに「パーシャル」「アセットタグ」「
- 古いブログ記事や社内ドキュメントで
- 「Layouts and Rendering ガイドの ○○セクションを参照」といったリンクや案内をしている場合、対応する場所が Action View Overview に移っている可能性があるので注意。
render inline:のように「存在はするが推奨されない API」については、ガイド上も「基本使わない」トーンが強まっているため、- これらに依存するコードを書く理由/必要性は、より慎重に検討すべき、というメッセージと読み取れます。
- JSON/XML/API 向けの説明が整理されているため、
- 新規開発者には「Ajax 用ビュー」ではなく「API エンドポイントとしてのコントローラ・レスポンス」という観点で教えるのが自然になります。
- 参考情報 (あれば)
- 該当 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
- Layouts and Rendering:
- MIME タイプの基礎:
- レスポンスヘッダだけ返す用途の例:
head :no_content(204 No Content): 成功だが本文不要な APIhead :not_found(404): リソースが存在しない場合に本文不要で返す
(これらはガイドのheadセクションで例示されている可能性があります)
#58628 Use new max_connections instead of pool in guide [ci skip]
マージ日: 2026/9/4 | 作成者: @rnestler
- 概要 (1-2文で)
Rails 8.1 でデータベース設定オプションpoolがmax_connectionsに名称変更されたことを受けて、公式ガイド内の記述を新しいオプション名に合わせて修正した PR です。コードや挙動の変更ではなく、ドキュメント(ガイド)のみを対象とした修正です。
- 変更内容の詳細
何が変わったか
対象ファイル
guides/source/action_cable_overview.mdguides/source/configuring.md
主な変更点
- これまでデータベース接続数を表す設定としてガイド中で使われていた
poolというキー名を、Rails 8.1 で正式に導入されたmax_connectionsというキー名に置き換えています。 poolという用語の説明やサンプル設定が、max_connectionsの説明・サンプル設定に更新されています。
- これまでデータベース接続数を表す設定としてガイド中で使われていた
想定されるガイド内の記述例(イメージ)
※PR本文からは正確な文面までは分かりませんが、Rails のガイド構成からすると、以下のような記述が pool → max_connections に書き換えられていると考えられます。
# 旧: 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: 5000Action Cable のガイド (action_cable_overview.md) についても、内部で利用するデータベース接続数に関連する設定例や説明部分が pool から max_connections に読み替えられています。
- 影響範囲・注意点
コード・挙動への影響
- この PR 自体はドキュメントのみの変更であり、Rails 本体の挙動には一切影響しません。
- ただし、Rails 8.1 を使う開発者がガイドを見たときに、正しい設定キー名(
max_connections)が参照できるようになるという意味で、実務上の混乱を防ぐ効果があります。
マイグレーションの観点
- Rails 8.1 からは
poolがmax_connectionsにリネームされています。 - 既存アプリの
database.ymlを Rails 8.1 に合わせる場合は、ガイドに倣って以下のような変更が必要になる可能性があります(※実際の互換性・非推奨期間などは CHANGELOG / 該当PR を要確認)。diff- pool: 10 + max_connections: 10 - 新たに Rails 8.1 でアプリを作る場合は、最初から
max_connectionsを使うべきです。
- Rails 8.1 からは
ドキュメントとの整合性
- ガイドが
poolではなくmax_connectionsを前提にすることで、- Active Record の新しい設定オプション群(
keepalive,max_age,min_connections,max_connections)との整合性が取れる - 「コネクションプールのサイズ」という概念が、より直感的な名前(最大接続数)で説明される
といったメリットがあります。
- Active Record の新しい設定オプション群(
- ガイドが
- 参考情報 (あれば)
- Rails 8.1 Active Record CHANGELOG(PR本文で言及)
keepalive,max_age,min_connectionsの追加pool→max_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-2文で)
Rails 8.1系(8-1-stableブランチ)で、ActiveSupport::TestCase.run_orderを扱うガード条件(安全な存在チェック)が誤っていたのを 1行だけ修正したバックポートPRです。Minitest 6 対応のフォローアップで、テスト実行順序の取得まわりで発生しうる不具合を防ぐ目的の修正です。
- 変更内容の詳細
※このPRは activesupport/lib/active_support/test_case.rb の 1 行差し替えのみです。
元PR (#56879) および Minitest 6 対応 (#56434) のフォローアップで、「AS::TC.run_order を参照する際の条件分岐(ガード)」が不適切だったのを修正しています。
典型的には、次のような処理が関係します(擬似コード):
module ActiveSupport
class TestCase < Minitest::Test
# テストの実行順序を Rails 側から制御/参照するコード
def self.test_order
# ここで Minitest::Test.run_order や ActiveSupport::TestCase.run_order を見たり、
# Minitest のバージョンによって存在しない場合があるメソッド/定数を
# 条件付きで呼び分ける
end
end
endMinitest 6 では API や挙動が微妙に変わっており、例えば:
run_orderが定義されていない状況で無条件に呼び出すとエラーになるnilや想定外の値を返す可能性を考慮していない条件式だと、テスト順序の設定が正しく反映されない
といった問題が起こりえます。
このPRでは、その「run_order を利用する前のチェック条件」だけを 1行修正し、
run_orderが存在しない/使えないケースでも例外にならない- Rails 側で意図した実行順序(例:
:random,:sortedなど)が正しく反映される
ようにしています。
実際の差分イメージ(あくまで例示):
- if AS::TC.respond_to?(:run_order)
+ if AS::TC.respond_to?(:run_order) && AS::TC.run_order
# run_order が有効なときだけ特定の処理を行う
endあるいは、Minitest 5 / 6 でのインターフェイス差異を正しく判定するようなガードに修正されています。
- 影響範囲・注意点
影響範囲
- Rails 8.1 系で Minitest を使っているプロジェクトの「テスト実行順序(test order)」周りに影響します。
- 特に Minitest 6 系と組み合わせたときに、
- テストスイートの実行開始時に
NoMethodErrorやundefined method 'run_order'のようなエラーが出ていたケース config.active_support.test_order、ENV['MT_RUN_ORDER']などと組み合わせた挙動が不安定だったケース
が安定することが期待されます。
- テストスイートの実行開始時に
互換性 / 破壊的変更について
- 1 行のガード修正のみであり、
run_orderが正しく提供されている環境ではほぼ影響はありません。 - 既存の Minitest 5 系を利用している Rails 8.1 プロジェクトでも、仕様上の挙動(ランダム実行やソート順など)は基本的に変わらない想定です。
- バグ回避のための条件式強化が中心で、「仕様変更」というより「正しい条件に合わせた」修正に近いです。
- 1 行のガード修正のみであり、
注意点
- 8.1 系を Minitest 6 と組み合わせている場合は、この修正が含まれるパッチレベルにアップデートしておくと安全です。
- Rails 本体側のテスト(特に
ActiveSupport::TestCaseを継承した独自テストクラス)で、run_orderを直接参照しているような高度なカスタマイズを行っている場合は、一度テストを走らせて想定通りの順序になっているか確認してください。
- 参考情報 (あれば)
- 元PR(本流ブランチ向け):
- #56879 — Minitest 6 対応の follow-up として
AS::TC.run_order周りを修正した PR
- #56879 — Minitest 6 対応の follow-up として
- 元の Minitest 6 対応 PR:
- #56434 — Rails が Minitest 6 で動作するようにするための対応一式
- このPR:
- #58665 は上記 #56879 のバックポート (
git cherry-pick -x e2feab4...) を8-1-stableに適用したものです。
- #58665 は上記 #56879 のバックポート (
#58666 Use equality comparison when fetching information about a single table
マージ日: 2026/9/4 | 作成者: @ilianah
- 概要 (1-2文で)
MySQL 用スキーマリーダが「単一テーブル」に対してもIN (...)を使ってinformation_schemaを問い合わせていた挙動を、1件の場合は=比較に戻すPRです。Vitess 環境でremove_indexなどが正しく動かずエラーになっていた問題を、古い Vitess バージョンも含めてワークアラウンドする目的があります。
- 変更内容の詳細
背景
- #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)
def quoted_scope(table_names)
# table_names は単一/複数問わず配列前提
"AND table_name IN (#{table_names.map { |t| quote(t) }.join(', ')})"
end変更後(1件なら =、複数なら IN)
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 程度の軽微なリファクタリング)。
- 影響範囲・注意点
影響範囲
- 対象:
- MySQL 系アダプタ(
mysql2など)を使った Active Record - スキーマ情報を
information_schemaから取得する処理全般remove_index/index_exists?/ マイグレーション実行時の index 情報参照 など
- MySQL 系アダプタ(
- 特に恩恵を受けるケース:
- 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 を使っても悪影響はほぼありません(単一テーブルクエリが=になるだけ)。
- Vitess 側に
- MySQL 単体利用・非 Vitess 環境
- ローカル MySQL や一般的なクラウド MySQL では、
IN ('users')と= 'users'は実質同じ意味なので、パフォーマンスや動作への影響はほぼありません。
- ローカル MySQL や一般的なクラウド MySQL では、
- 参考情報 (あれば)
- 元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-2文で)
Rails のルーティングテスト用アサーションassert_recognizes/assert_routingにおいて、method: :all指定時にPATCHと新たに追加されたQUERYメソッドも検証対象とするように修正した PR です。これにより、「全ての HTTP メソッドを受け付けるはず」という前提で書かれたテストが、実際にはPATCHやQUERYを受け付けていないルートを正しく検出できるようになります。
- 変更内容の詳細
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]テスト側で:
rubyassert_routing "/posts/1", { controller: "posts", action: "update", id: "1" }, method: :allと書くと、「全ての HTTP メソッドにマッチする」と主張しているにもかかわらず、実際には
PATCHやQUERYは通っていない状態でテストが通ってしまう、という不整合があった。このリスト (
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 を追加しました。
※ 実際のコード断片(イメージ):
# 変更前(イメージ)
HTTP_METHODS_FOR_ALL = [:get, :post, :put, :delete].freeze
# 変更後(イメージ)
HTTP_METHODS_FOR_ALL = [:get, :post, :put, :delete, :patch, :query].freezeこれにより、以下のようなテストが:
assert_routing "/articles/1",
{ controller: "articles", action: "show", id: "1" },
method: :all内部的には少なくとも:
[: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ルートから自動的に提供されるため、個別チェックは不要と見なした。
- Rails では一般に
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に、本変更によって挙動が厳密になり、既存のテストが失敗する可能性があることが明記されています。
- 影響範囲・注意点
3-1. 既存テストへの影響
最も重要な影響点は、「今まで通っていたテストが落ちる可能性がある」ことです。
条件としては:
- テストコード:
assert_recognizesまたはassert_routingでmethod: :allを指定している
- ルーティング定義:
- 対象のルートが
PATCHまたはQUERYを受け付けていない (via: [:get, :post, :put, :delete]のような定義 など)
- 対象のルートが
この場合、今回の修正後は PATCH / QUERY でもそのパスが解決されることを期待されるため、テストが失敗します。
これは PR の意図通りのブレイキング変更であり、「本当にすべての HTTP メソッドを受け付けるべきルートか?」を見直すきっかけになります。
3-2. 対応パターン
テストが落ちた場合の代表的な対応は次の 3 つです。
ルート定義を更新して本当に “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のテストを維持する。テストが “全メソッド” である必要はなかった場合
もともと「特定のメソッドだけ通ればよい」ルートであるのに、便宜上
method: :allを使っていた場合は、明示的にメソッドを指定する:rubyassert_routing "/posts/1", { controller: "posts", action: "update", id: "1" }, method: :patch # あるいは :get, :post など必要なものだけもしくは、
[:get, :post]のように自前でループを書くなど。QUERY をサポートしない方針の場合の検討
- Rails 側に QUERY が追加されたこと自体を念頭に置き、自アプリで QUERY をサポートするかどうか設計から見直す必要がある場合があります。
- 現時点で QUERY を完全に使わないポリシーなら、
method: :allは使わず、必要なメソッドに限定したテスト戦略を取る方が明確です。
3-3. 新規開発時のベストプラクティスへの示唆
- 「
via: :allでルートを定義し、テスト側でもmethod: :allを使う」ことがより正しく噛み合うようになりました。 PATCHが更新系のデフォルトであること、QUERYメソッドが Rails に追加されていることを前提に、本当に全メソッドを許可したいケース以外ではvia:/method:の指定を明示的かつ限定的にするのが安全です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58623
- 関連コード:
actionpack/lib/action_dispatch/testing/assertions/routing.rbactionpack/test/controller/resources_test.rbactionpack/CHANGELOG.md
assert_routing/assert_recognizesドキュメント (Rails Guides / RDoc) も今回の変更でmethod: :allが明文化されているため、最新版の公式ドキュメント参照が推奨されます。
#58651 SchemaContext#attributes: preload schemas to prevent deadlock
マージ日: 2026/9/3 | 作成者: @kyrofa
- 概要 (1-2文で)
SchemaContext#attributesでRactor.store_if_absentによるロックを使ってAttributesを初期化していた結果、スキーマ読み込み時に再帰的に同じロックを取りに行ってデッドロックが発生しうる問題を解消した PR です。
ロックを完全に取り除き、デッドロックを防ぎつつ動作をテストで担保しています。
- 変更内容の詳細
問題の構造
SchemaContext#attributes は「そのクラスの Attributes オブジェクト」を遅延初期化する箇所です。元の実装イメージは概ね次のような流れになっていました:
SchemaContext#attributesが呼ばれるRactor.store_if_absentを使って、Attributesをスレッド/Ractor 安全にキャッシュ- その際
Attributes.newが呼ばれる Attributes.new内でAttributeRegistration#apply_pending_attribute_modificationsを実行- ここでスーパークラス側の attribute 修飾(例:
EncryptableRecordの暗号化関連)を適用するため、クラス階層を遡りながらcolumns_hashを呼ぶ columns_hashがスキーマのロードをトリガし、load_schema→ 再度SchemaContext#attributesを呼ぶ- 2 に戻り、同じ
Ractor.store_if_absentでロックを取りに行く → 再入ロック不可なためデッドロック
つまり「Attributes 初期化 → attribute 修飾の適用 → スキーマロード → 再度 Attributes 初期化」のループの途中で、同じロックを再取得しようとして固まる、という構造的な問題です。
この PR の修正
修正の核心は「SchemaContext#attributes 内で使っていた Ractor.store_if_absent によるロックを削除する」ことです。
変更点は以下の通りです:
activerecord/lib/active_record/model_schema/schema_context.rbSchemaContext#attributesからRactor.store_if_absent呼び出しを削除- 代わりに、ロックなしで
Attributesを生成・利用する形に変更 - 行レベルでは +1/-3 なので、ほぼ「ロックまわりの削除」のみ
擬似コードでの Before / After は以下のようなイメージです(実際のコードは多少異なりますが概念的にはこうです):
# 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 修飾を含むような実運用に近いシナリオをカバーしていると考えられます
- デッドロックが再発しないことを保証する回帰テストとして機能
- 再帰的に
- 影響範囲・注意点
影響しうる領域
- Active Record のスキーマロード周り (
load_schema,columns_hash) - モデルの attributes 定義とその修飾(
AttributeRegistration, 暗号化属性を提供するEncryptableRecordなど) - Ractor / 並列実行環境での Attribute キャッシュ
これらを使うすべてのアプリケーションが潜在的な影響範囲です。
デッドロック解消の代償としての懸念点
- ロック削除により、「同時に複数の Ractor/スレッドが同じクラスの Attributes を初期化しに行く」ケースで
- 同じ
Attributesに対して重複初期化が発生する可能性 - ただし Active Record の attribute 定義は基本的に idempotent かつスキーマ/メタデータであり、多少の重複計算は許容される設計になっていることが多い
- 同じ
- 本 PR は「安全性よりも、致命的なデッドロックを確実に回避する」ことを優先した修正といえる
実運用での注意点
- Rails アプリで以下のような条件がそろうときに、この修正の恩恵を強く受けます:
- モデルに暗号化属性などの attribute 修飾(
EncryptableRecordやそれに準ずるモジュール)を利用している - 並列実行/Ractor を利用している、もしくは並列なスキーマロードが起こりうる
- 起動時や初回アクセス時にランダムにハングするような現象があった場合、このバグに該当していた可能性がある
- モデルに暗号化属性などの attribute 修飾(
- PR #58650 に紐づくバグであり、その Issue に類似する症状(スキーマロード中に固まる)が起きていた場合、Rails バージョンをこの修正を含むものまで上げる価値があります。
- 参考情報 (あれば)
- この PR が修正する Issue: #58650
(SchemaContext#attributes/Ractor.store_if_absentに起因するデッドロック報告) - 関連クラス・メソッド:
ActiveRecord::ModelSchema::SchemaContext#attributesActiveRecord::AttributeRegistration#apply_pending_attribute_modificationsActiveRecord::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-2文で)
Rails のActionDispatchが持つ「デフォルトレスポンスヘッダ設定」を Ractor 安全性のために freeze する際、Rails.application.config.action_dispatch.default_headersそのものが凍結されてしまう問題を修正し、設定オブジェクト自体は凍結されないようにした PR です。これにより、古くから存在する「load hook 内で config を破壊的変更する」ようなコードがクラッシュせず動作(※実質的な効果は元々ほぼなかった)を続けられるようになります。
- 変更内容の詳細
背景
以前のコミット(
56d19e5...)で、Ractor 対応の一環として「Action Dispatch のデフォルトヘッダ」を freeze するようになった。その際に「ヘッダの中身」だけでなく、「
Rails.application.config.action_dispatch.default_headers(設定オブジェクト)そのもの」も結果的に freeze されてしまっていた。そうすると、以下のように load hook の中で
default_headersを破壊的変更しようとするコードがクラッシュするケースが出る:rubyinitializer :mutation do ActiveSupport.on_load(:action_controller) do Rails.application.config.action_dispatch.default_headers.clear end endActionController::Baseが先にロードされるとActionDispatch::Responseもロードされ、そのタイミングでdefault_headersが freeze される。- その後に
ActionController::APIをロードすると load hook がもう一度呼ばれ、2 回目のclearが「frozen なオブジェクトへの変更」となり例外が発生する。
このようなコードは設計的には「やるべきではない」ものの、10 年ほど問題なく動いていたため、いきなり壊すのは避けたいという判断になっています。
具体的な修正
変更ファイルは 2 つだけです。
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をその後に変更しても、実際のレスポンスヘッダには反映されない(これは元々もほぼ同様の挙動)。
railties/test/application/configuration_test.rb(+11 / 0)テストが追加されています。主に次のようなことを確認しています:
ActionDispatch読み込み後でも、Rails.application.config.action_dispatch.default_headersが freeze されていないこと。- つまり、
config.action_dispatch.default_headers.clearや他の破壊的操作を行っても「FrozenError」等を投げずに動くこと。
- 影響範囲・注意点
影響範囲
影響を受けるのは主に以下のようなコードを持つアプリ/ライブラリです:
ActiveSupport.on_load(:action_controller)や他の load hook 内でRails.application.config.action_dispatch.default_headersを破壊的に変更しているコード。- 特に「
ActionController::BaseとActionController::API両方がロードされる環境」で、同じ load hook が複数回呼ばれる場合。
この PR 適用後:
- これらのコードは 例外なく動作を継続 します。
- ただし、
ActionDispatch::Responseが実際に使うデフォルトヘッダは freeze 済みのコピーなので、- 「Response 実行中のヘッダ挙動」がこれらの後付け変更で書き換わることはなく、
- 設定オブジェクトを書き換えても「見かけ上書き換えられたように見えるが、実際のレスポンスに影響しない」状態になります(これは仕様として以前からほぼ同じで、新たな挙動ではない)。
注意点・ベストプラクティス
PR の説明にもある通り、load hook 内で
Rails.application.configを破壊的変更するのは推奨されません。- 設定は通常、環境ごとの config ファイル(
config/environments/*.rbなど)で「起動時に一度だけ」行うのが筋です。 - 特に Ractor 対応以降は、マルチスレッド/マルチ Ractor 環境でも予測可能な挙動を維持するため、設定値は immutability を前提として扱う傾向が強まっています。
- 設定は通常、環境ごとの config ファイル(
もし「ActionDispatch::Response が使うデフォルトヘッダをアプリケーション側から確実に制御したい」場合は、
- 起動時に
config.action_dispatch.default_headers = { ... }のように設定し、 - それ以降は変更しない(変更しても意味がない)という前提で設計した方が安全です。
- 起動時に
- 参考情報 (あれば)
元になった 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-2文で)
PostgreSQL 18.4 未満の環境でdisable_referential_integrity実行中に例外が発生した場合でも、必ずトリガーが再有効化されるように修正した PR です。これにより、テストやクリーナー処理の失敗をきっかけに、DB 全体の外部キー制約が恒久的に無効化されてしまうバグを防ぎます。
- 変更内容の詳細
問題の背景
PostgreSQL 18.4 より前のバージョンには、外部キーを一時的に「NOT ENFORCED」にする機能がないため、Rails の
disable_referential_integrityは内部的にsqlALTER TABLE ... DISABLE TRIGGER ALL; -- ブロック実行 ALTER TABLE ... ENABLE TRIGGER ALL;というカタログ(テーブル定義)レベルの変更を行います。
元の実装では
ENABLE TRIGGER ALLがensureで保護されていなかったため、ブロック内で例外が発生すると「トリガーを有効に戻す処理」が実行されず、- 外部キー用トリガー(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 ALL を ensure ブロックに移動
activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rbのdisable_referential_integrity(PostgreSQL 用実装)で、トリガー再有効化処理をensureに移動しました。
概念的には以下のようなイメージです(簡略化した 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に新しいテストを追加:ブロック内で例外を発生させる:
rubyconnection.disable_referential_integrity do raise "boom" endrescue した後、子テーブルに「無効化されたトリガー」が残っていないことを確認するテストです。
pg_triggerテーブルを参照し、tgenabled = 'D'(Disabled)なトリガーが残っていないかをチェックする形。
テストは以下の条件で skip されます:
- PostgreSQL 18.4 以上の場合
→ 実装パスが異なり、このバグ自体が起こらないため。 - テスト実行ユーザが superuser でない場合
→DISABLE TRIGGER ALLは superuser でないと実行できず、その場合 no-op になってしまうため。
no-op だとテストが「たまたま」通ってしまい、回帰検知にならないため。
- PostgreSQL 18.4 以上の場合
4) CHANGELOG の更新
activerecord/CHANGELOG.mdに、本修正に関する記述が追加されています(バグ修正であること、PostgreSQL の referential integrity に関する変更であることなど)。
- 影響範囲・注意点
影響範囲
主に影響を受けるのは、PostgreSQL 18.4 未満かつ Rails の
disable_referential_integrityを利用しているケースです。truncate_tablesを使うテストヘルパ- DatabaseCleaner の
truncationstrategy - その他、手動で
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 バージョン切り替え時に、「どのパスが使われているか」を意識しておくとデバッグしやすくなります。
- 参考情報 (あれば)
- 元 PR: #58635
- 関連 PR:
- #50196 — fixtures 利用時に
disable_referential_integrityをトランザクション内で実行するようにした対応 - #57378 — PostgreSQL 18.4+ の
NOT ENFORCEDを使った referential integrity の新実装
- #50196 — fixtures 利用時に
- 関連する PostgreSQL の仕様:
ALTER TABLE ... DISABLE TRIGGER ALL/ENABLE TRIGGER ALLはセッション状態ではなくカタログ状態を変更する点に注意(トランザクションに包まないと永続的な変更になる)。
#58638 Add PostgreSQL adapter support for configuring error_verbosity
マージ日: 2026/9/3 | 作成者: @beauraF
- 概要 (1-2文で)
PostgreSQL アダプタに、接続設定 (database.yml) からerror_verbosityを指定して libpq のエラーメッセージ詳細度を制御できる機能が追加されました。これにより、ユニーク制約違反などのエラーに含まれる PII(メールアドレス等)がログ経由で漏洩することを抑制しやすくなります。
- 変更内容の詳細
2-1. 何ができるようになったか
config/database.yml の各環境ごとの設定に、error_verbosity キーを追加して、PostgreSQL 接続のエラーメッセージ詳細度を指定できます。指定された値は、接続確立後に PostgreSQLAdapter#configure_connection 内で一度だけ PG::Connection#set_error_verbosity に渡されます。
サンプル:
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.rbconfigure_connectionメソッド内で、config[:error_verbosity]が存在する場合のみ@connection.set_error_verbosity(config[:error_verbosity])を実行するロジックが追加。- 既存の
encoding設定と同じパターンで、「設定されていれば一度だけ適用」という方針。
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rberror_verbosityが設定されたときにPG::Connection#set_error_verbosityが呼び出されることを検証するテストが追加。
activerecord/CHANGELOG.md- Active Record への新機能として、「PostgreSQL アダプタで error_verbosity を設定できる」ことが追記。
コードイメージ(Rails 内部の処理イメージ・擬似コード):
def configure_connection
super
if config[:error_verbosity]
@connection.set_error_verbosity(config[:error_verbosity])
end
end- 影響範囲・注意点
デフォルト動作は変わらない
error_verbosityを指定しなければ、これまで通りpg/ PostgreSQL のデフォルト設定(PQERRORS_DEFAULT)が利用されます。- 既存アプリは、この設定を追加しない限り挙動は変わりません。
PII流出対策としての有効性
- ユニーク制約違反、外部キー制約違反、デッドロックなどのエラーの
DETAILフィールドには、実際に違反した値(例: メールアドレス、電話番号)が含まれます。 ruby-pgはこのテキストをそのまま例外メッセージに含めるため、ログやエラートラッカー(Sentry など)に PII が載りがちでした。error_verbosityをPG::PQERRORS_TERSE等に下げることで、この詳細テキストが抑制されるため、ログの情報量とプライバシー保護のバランスを取りやすくなります。
- ユニーク制約違反、外部キー制約違反、デッドロックなどのエラーの
設定値は Ruby コードとして評価されることに注意
- ドキュメント例でも
<%= PG::PQERRORS_TERSE %>のように ERB でPGの定数を参照しています。 - 誤って文字列
"PG::PQERRORS_TERSE"のように書くと正しく動作しないので、「Ruby の定数として評価させる」記法にする必要があります。
- ドキュメント例でも
再接続・コネクションプールとの関係
configure_connectionは接続確立時に呼ばれるので、プールされた各コネクションごとに一度だけ適用されます。- 接続切断→再接続が起きても、そのタイミングで再びこの設定が適用されます。
依存バージョン
PG::Connection#set_error_verbosityはpggem / libpq が対応している必要があります。- 通常のサポート対象バージョンの Rails + pg であれば問題ないはずですが、極端に古い
pgを使っている場合は事前に確認が必要です。
- 参考情報 (あれば)
libpq ドキュメント(PQsetErrorVerbosity)
https://www.postgresql.org/docs/current/libpq-control.html#LIBPQ-PQSETERRORVERBOSITYpggem ドキュメント(PG::Connection#set_error_verbosity)
https://deveiate.org/code/pg/PG/Connection.html#method-i-set_error_verbosity想定される設定例(本番環境で PII を抑えたい場合):
yamlproduction: adapter: postgresql database: my_app_production error_verbosity: <%= PG::PQERRORS_TERSE %>逆に、開発環境で詳細なデバッグ情報が欲しい場合:
yamldevelopment: adapter: postgresql database: my_app_development error_verbosity: <%= PG::PQERRORS_VERBOSE %>
#58647 Make the controller configs shareable in ractorize!
マージ日: 2026/9/2 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Railsアプリをractorize!した際に、コントローラのコンフィグ(設定オブジェクト)が Ractor 間で安全に共有できるようにした PR です。アプリケーションブート後の「コントローラレベルの設定」は共有可能な形に固定し、各コントローラインスタンス側で変更可能なコピーを持つ設計に寄せています。
- 変更内容の詳細
※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行)。
典型的には、以下のような処理が入っているイメージです(あくまで擬似例):
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 対応」できているかをテストした。 - 例として考えられるテストイメージ:
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 になっているかを確認するテストが入っています。
想定されるテストイメージ:
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 が残っていないか」を回帰テストとして担保しています。
- 影響範囲・注意点
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 しようとしてエラー、あるいは想定外の動作)。
例:
# 悪い例(本番起動後や ractorize! 後にやるべきでない)
PostsController.config.some_option = :new_valueこのような用途が必要な場合は:
- コントローラインスタンスに紐づく設定(
@config的なもの)があるならそちらを変更する - あるいは「実行時フラグ」「Feature flag」など別の仕組みを使う
といった方向が推奨されます。
3-2. Ractor を使わないアプリへの影響
- 変更箇所は主に
Rails.application.ractorize!経由のパス、および shareable テストの追加であり、 - 通常の(Ractor を使わない)Rails アプリでは基本的に挙動は変わらないはずです。
- ただし、将来的な Ractor 対応を見据え、**「ブート後にクラスレベルの設定をむやみに書き換えない」**という前提が一層強くなった、と理解しておくと良いです。
3-3. マルチプロセス / スレッド環境との関係
- 以前から推奨されていた「起動後にグローバルな設定オブジェクトをいじらない」方針と整合的です。
- Ractor 特有の shareable 制約が明文化された形になるため、スレッドセーフ設計との相性もよく、「設定は起動時確定、以後は読み取り専用」という構成に寄せると安全です。
- 参考情報 (あれば)
- 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つと位置づけられます。
- Rails の Ractor 対応系 PR は、
このPRにより、「Ractor化した Rails アプリでコントローラ設定を安全に共有できる」基盤が整ったと整理できます。
#58653 Freeze Active Record specific on callbacks
マージ日: 2026/9/2 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Active Record のトランザクション系コールバック(after_create_commitなどのonオプション付き)を Ractor でも共有可能(shareable)にするため、これらのコールバック定義を freeze する変更です。これにより、Ractor モード:raiseでアプリ起動時に落ちてしまう問題が解消されます。
- 変更内容の詳細
背景
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 つです。
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 から安全に参照できるようになります。
activerecord/test/cases/transaction_callbacks_test.rb- テストが 19 行追加されており、以下のような点を確認していると考えられます:
- トランザクション系のコールバック (
after_commit,after_create_commit,after_update_commit,after_destroy_commitなど) に設定されるonオプションが freeze されていること。 - freeze されても、従来通りコールバックが正しいタイミングで呼ばれること(機能面に影響がないこと)。
- トランザクション系のコールバック (
- テストが 19 行追加されており、以下のような点を確認していると考えられます:
- 影響範囲・注意点
対象となる機能
- Active Record のトランザクション系コールバック全般
(after_commit,after_rollback,after_create_commit,after_update_commit,after_destroy_commitなど) - 特に
on: ...オプションを内部的に利用している部分。
- Active Record のトランザクション系コールバック全般
アプリケーションコードへの影響
通常の利用(
after_create_commit :fooなど)では挙動は変わりません。ただし コールバック定義時に渡されるオプションオブジェクトを後からミューテート(破壊的変更)していた場合、freeze により
FrozenErrorになる可能性があります。
例:(やってはいけないが、もしやっていた場合)rubyoptions = { on: [:create] } after_commit :foo, options options[:on] << :update # ここで FrozenError になるようになる可能性そもそもこのような使い方は非推奨であり、Rails 的にも不変(immutable)である前提のほうが望ましいため、この変更は「仕様として筋が良い」方向です。
Ractor を使っていないアプリ
- 実質的な挙動変更はほぼなく、パフォーマンスや互換性への影響は軽微と考えられます。
Ractor モードを使う/検証したいアプリ
- Ractor モード
:raiseを有効にしている/検証している環境では、トランザクションコールバックを持つモデルが eager load されてもクラッシュしなくなります。 - Ractor 対応を進める上での前提条件(Active Record コールバックが shareable であること)が一つ満たされます。
- Ractor モード
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58653
- 関連箇所(概念的な参考)
ActiveRecord::Transactionsモジュール- トランザクションコールバック関連の API:
- Ruby Ractor の shareable オブジェクト仕様:
この PR は小さい差分ですが、Rails の Ractor 対応を一歩進めるための基盤的な変更と位置付けられます。
#58639 Preserve fetch_multi key order when local cache is active
マージ日: 2026/9/2 | 作成者: @moizafzal936
- 概要 (1-2文で)
ActiveSupport::Cache::Store#fetch_multiが「渡したキー順で結果を返す」という仕様を、ローカルキャッシュ (Strategy::LocalCache) 使用時にも正しく守るようにしたバグ修正です。ローカルキャッシュ命中/ミスの有無に関わらず、呼び出し側が指定したキー順で結果が返るようになります。
- 変更内容の詳細
何が問題だったか
ActiveSupport::Cache::Store#fetch_multi は、ドキュメント上「引数に渡したキーの順序で値を返す」と明記されています (#34700 で導入)。
しかし Strategy::LocalCache が有効な場合のオーバーライド実装では、次のような流れになっていました:
- ローカルキャッシュに既に存在するキー(ヒット)を先に
resultsに詰める - ローカルキャッシュに無いキー(ミス)だけを
super(下位ストアのfetch_multi)で取得してresultsにマージ
それぞれのステップ単体では順序は保たれているものの、「ヒットしたキー群 → ミスしたキー群」という順番で連結されるため、呼び出し元が渡したキーの順序とは異なる並びで返ってしまうケースがありました。
例:
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オプションや「ミスの記録」などの挙動は変えない
という条件を満たしたまま、順序だけを修正できます。
イメージ:
# 擬似コードイメージ(実際の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.mdfetch_multiのキー順序がローカルキャッシュ使用時にも保存されるようになったことを明記
なお、Issue 側で挙げられていた「write_multi の後に fetch_multi をすると、古い値が返ってくるのでは」という懸念については、PR説明によると仕様どおりであり、本PRでは順序の問題のみにフォーカスしています。
- 影響範囲・注意点
影響範囲
cache.with_local_cacheなどでローカルキャッシュを有効化した状態でfetch_multiを使っているコード全般- Redis キャッシュストア、MemCache など
Strategy::LocalCacheをミックスインしているストアが対象
互換性
- キーの「どれが含まれるか」や取得される値自体のロジック(ヒット/ミス判定、
skip_nil、ミスの記録など)は変わりません - 変わるのは返り値のハッシュのキー順のみです
- これまで「ローカルにヒットしたキーが先に並ぶ」実装に依存していた場合は、順序が変わることに注意してください
- 正しくは「渡したキー順で返る」ことに依存すべきであり、今回の修正はドキュメントどおりの挙動への是正です
- キーの「どれが含まれるか」や取得される値自体のロジック(ヒット/ミス判定、
テスト
- 既存のテストに加え、回帰テストが共通のBehaviorとして入っているため、今後別ストアの変更などによる再発リスクは低減されています
- 参考情報 (あれば)
- 対応Issue: #58637
- 元仕様導入PR: #34700 (
fetch_multiがキー順を保証するようになった変更) - 関連クラス:
ActiveSupport::Cache::Store#fetch_multiActiveSupport::Cache::Strategy::LocalCache
#58642 Freeze Active Record time zone related configuration
マージ日: 2026/9/2 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails 7 での Ractor 対応の一環として、Active Record の「タイムゾーン関連設定」をフリーズ(凍結)し、Ractor 間の共有時に起きる不変性違反・Isolation 問題を解消する PR です。これにより、属性セット(attribute set)が Ractor ごとに遅延構築されても、安全に設定を共有できるようになります。
- 変更内容の詳細
背景・問題点
#58578 により、Active Record の「attribute set(カラム定義などを含むメタ情報)」は:
- 以前:
load_schema時に一括・事前ロード(eager load) - 現在: Ractor ごとに遅延構築(lazy, per ractor)
- 以前:
その結果、次のようなコードが動かなくなっていました:
rubyPost.create! Ractor.new do Post.create! end理由:
- Ractor 内で attribute set が再構築される過程で、Active Record の「タイムゾーン関連設定」(例:
config.active_record.default_timezoneやtime_zone_aware_attributesなど)にアクセスする。 - これらの設定オブジェクトが Ractor-safe(不変 / deep-freeze)ではないため、Ractor 間で共有すると isolation ルールに違反してしまう。
- Ractor 内で attribute set が再構築される過程で、Active Record の「タイムゾーン関連設定」(例:
この 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_timezoneconfig.active_record.time_zone_aware_attributesconfig.active_record.time_zone_aware_typesconfig.active_record.active_record.default_timezone周辺の内部設定
実際のコードでは、これらの設定を参照している内部構造(配列・ハッシュなど)もフリーズ対象にしていると考えられます。
- 影響範囲・注意点
影響範囲
Ractor を使う Rails アプリ
- Ractor 内で Active Record モデルを初回利用する際に attribute set が構築されても、タイムゾーン設定が Ractor-safe になったことでエラーなく動作するようになります。
- とくに並列処理で
Ractor.new { Post.create! }などを行うケースが対象。
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 は起動時に一度行われるだけ)。
- 参考情報 (あれば)
- 関連 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-2文で)
ActionPack 内の一部設定をmattr_accessorベースのクラス変数管理から、インスタンス変数ベースの設定管理に切り替える変更です。特にActionDispatch::Requestの MIME ネゴシエーション設定(ignore_accept_header)などで、「インスタンスからクラス設定が書き換わる」不自然な挙動をやめ、より一貫した設定の扱いにしています。
- 変更内容の詳細
2-1. MimeNegotiation (mime_negotiation.rb)
元の挙動の例として PR で挙げられているのが以下です:
ActionDispatch::Request.ignore_accept_header
# => false
ActionDispatch::Request.new({}).ignore_accept_header = true
# => true
ActionDispatch::Request.ignore_accept_header
# => trueここでは ignore_accept_header が mattr_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 ベースで見たときに問題がないこと)
がテストで裏づけられています。
- 影響範囲・注意点
3-1. インスタンスからクラス設定を書き換えるコードは壊れる可能性
次のようなコードを書いている場合は動かなくなる/意味が変わる可能性が高いです:
# 非推奨な使い方の例(元々ドキュメント化されていない)
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::Request や ExceptionWrapper を直接拡張・モンキーパッチしている場合は、この PR の差分を一度確認して、クラス変数/mattr_accessor に直接依存していないかをチェックする必要があります。
- 参考情報 (あれば)
- この PR は
#58620と同系統の変更で、ActionPack 全体で「設定をインスタンス変数ベースに寄せる」リファクタリングの一部と位置付けられています。 - 関連するコードは主に以下:
actionpack/lib/action_dispatch/http/mime_negotiation.rbactionpack/lib/action_dispatch/middleware/exception_wrapper.rb- テスト:
actionpack/test/dispatch/request_test.rbactionpack/test/dispatch/exception_wrapper_test.rb
- Rails の設定まわりの設計方針として、「
mattr_accessor/ クラス変数を減らし、フレームワーク設定オブジェクトとインスタンス変数側に寄せる」流れが続いており、この PR もその一環です。
#58643 Store the AssetTagHelper settings in singleton class attributes
マージ日: 2026/9/2 | 作成者: @etiennebarrie
- 概要 (1-2文で)
AssetTagHelper が持つ6つの設定値の保持方法を、クラス変数からモジュール自身のインスタンス変数(単一オブジェクト上の属性)へ変更し、インスタンス経由でグローバル設定を書き換える仕組みを削除して、Ractor 対応と設計の明確化を図る変更です。Rails 7以降で予定される Ruby 4.0 / Ractor 環境でも AssetTagHelper の設定が安全に参照できるようにすることが主目的です。
- 変更内容の詳細
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 互換的になります。
- Ractor 間でクラス変数を共有しない Ruby 4.0 の仕様に依存せず、
※ PR本文では「Use instance variables on the module directly」と記載されている箇所がこれに該当。
2) 「委譲用の reader メソッド」を追加
実際に
image_tag,javascript_include_tagなどのアセットタグヘルパーからは、
直接インスタンス変数にアクセスするのではなく、reader メソッドを経由して設定値を取得するように変更。構造イメージ(擬似コード):
rubymodule 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 バージョンアップでのリグレッションを防ぎます。
- 影響範囲・注意点
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 がある場合は、
これを機に 公式にサポートされる設定 API(Rails.application.config.action_controller.asset_hostなど)への
依存に切り替える必要があります。 - AssetTagHelper の内部実装(変数の種類や場所)に直接依存するのは今後ますます壊れやすくなるため、
本PRは「内部実装への依存をやめてほしい」というメッセージとも解釈できます。
- 参考情報 (あれば)
- 本PRが参照している前段PR:
- https://github.com/rails/rails/pull/58627
→ 同様に「グローバル設定をクラス変数で持ち、インスタンス writer がグローバルを書き換える」ような箇所を
Ractor 互換な形へリファクタリングしていると考えられます。
- https://github.com/rails/rails/pull/58627
- Ractor とクラス変数の制約(Ruby 側の事情):
- Ruby 3 系以降、Ractor ではグローバル可変状態の扱いが厳しくなっており、 クラス変数やグローバル変数は基本的に Ractor 間で安全に共有できない方向の設計です。
- Rails はこの流れに合わせ、設定値の持ち方を徐々に Ractor フレンドリーな形へ移行しています。
#58629 Make the allow_browser callback shareable
マージ日: 2026/9/2 | 作成者: @etiennebarrie
- 概要 (1-2文で)
allow_browserの「未サポートブラウザに対するデフォルト動作」が Ractor でも共有可能なオブジェクト(shareable proc)になるよう修正された PR です。あわせて、Ruby の制約に対応するため、オプションキーワード引数:blockの扱いが変更されています。
- 変更内容の詳細
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 として定義し直されています。
コード断片イメージ(※実際のコード構造のイメージです):
# もともとクラス定義内のメソッドで直接 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
:blockas 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 内でのキャプチャに制限がかかるという仕様上の制約があります。
そのため、以下のような形の修正が入っています。
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: キーワードの扱いまわりがカバーされているはずです。)
- 影響範囲・注意点
通常のシングルスレッド / マルチスレッド 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 側で安全なパターンに寄せているため、実務的には問題になりにくい想定です。
- 参考情報 (あれば)
変更ファイル
actionpack/lib/action_controller/metal/allow_browser.rbactionpack/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 へのキャプチャ時の制約
- Rails ガイド (Edge Guides): Action Controller Overview / Browser support /
#58645 Fix TypeCaster sometimes leaking connection
マージ日: 2026/9/2 | 作成者: @skipkayhil
- 概要 (1-2文で)
TypeCaster が内部で使う DB コネクションを「借りっぱなし」にしてしまうケースがあり、接続プールからコネクションがリークする不具合を修正した PRです。Executor によるクリーンアップに依存せず、with_connectionを使って即座にコネクションを返却するように変更されています。
- 変更内容の詳細
変更ファイルは 1 件で、実質 1 行のメソッド呼び出し差し替えです。
activerecord/lib/active_record/type_caster/connection.rb 内で、TypeCaster がデータベース接続を取得する際のコードが、(推測される形として)以下のように変わっています:
# 変更前(例示)
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 行なので、だいたい次のような単純な呼び出し変更です:
# 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 側だけで完結してコネクションの貸し出し/返却を管理するように修正されています。
- 影響範囲・注意点
影響範囲
- ActiveRecord の TypeCaster が DB にアクセスして型情報を得るようなケース(例: カラム情報の解決など)で、内部的に利用される DB コネクションの扱いが変わります。
- 特に Live actions やストリーミングレスポンスなど「リクエストライフサイクルが長い」コンテキストで、接続プールのコネクション枯渇やリークが発生していた環境で効果が出ます。
- 通常のリクエスト/レスポンスでも、より確実にコネクションがプールに戻る形になるため、コネクション使用の健全性が高まります。
注意点
- 互換性への影響はほぼありません。API シグネチャの変更はなく、内部実装の改善に留まっています。
- もし独自に
TypeCasterまわりを拡張してconnection_pool.connection取得をラップしているようなメタプログラミングをしている場合は、with_connection前提の実装になったことを念のため確認するとよいです(通常はそのようなケースは稀です)。 - コネクションが早めに返却されることにより、「同じスレッドがずっと同じコネクションを保持する」ことを前提にしたようなコードがあれば(これ自体がアンチパターンですが)、挙動が変わる可能性があります。
- 参考情報 (あれば)
- 関連 Issue: #58644
- 内容: TypeCaster 利用時にコネクションがリークし、特に Live actions で接続プールが枯渇する問題の報告。
- Rails コネクションプール API:
ActiveRecord::ConnectionAdapters::ConnectionPool#with_connection- ブロック内でコネクションを貸し出し、ブロック終了時に必ずプールへ返却する公式かつ推奨の使い方。
- 背景技術:
- Rails では Executor(
ActiveSupport::Executor)を通じて、スレッドまたはリクエスト単位でのリソースライフサイクル管理(run!/complete!)を行っていますが、Live actions など特殊なライフサイクルではこれに完全には乗らないケースがあり、そのギャップを埋めるための修正と言えます。
- Rails では Executor(
#58578 Ractorize Active Record schema context
マージ日: 2026/9/2 | 作成者: @gmcgibbon
- 概要 (1–2文で)
Active Record のSchemaContextを、Ractor(マルチスレッド/マルチRactor環境)でも安全に扱えるように再設計したPRです。SchemaContextの中にAttributesオブジェクトを埋め込み、スキーマ関連の状態を Ractor ごとに分離しつつ、モデルクラス側の ivar はメイン Ractor で遅延評価できるように整理しています。
- 変更内容の詳細
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 で扱う という切り分けが明確になります。
- 「Ractor ローカルな状態」…
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 ごとのスキーマ状態を使うような経路に
- モデルインスタンスの attributes 管理が、新しい
activerecord/lib/active_record/model_schema.rb- モデルのスキーマ設定(
table_name,primary_key,inheritance_columnなど)を扱うメソッドが、- Ractor ローカルな情報は
SchemaContext - グローバルかつ遅延評価すべき情報はモデルクラス ivar に適切に分離されるよう処理をリファクタリング
- Ractor ローカルな情報は
- モデルのスキーマ設定(
※PR本文にコード例は無いですが、概念的には以下のようなイメージになります:
# 旧: 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
end2-3. テストまわりの変更
activerecord/test/cases/attributes_test.rb- 属性定義・読み書きロジックが、新しい
SchemaContext実装でも変わらないことを確認するように更新 - 既存の仕様を壊していないかの回帰テスト寄りの変更
- 属性定義・読み書きロジックが、新しい
activerecord/test/cases/core_test.rbactiverecord/test/cases/primary_keys_test.rbactiverecord/test/cases/relation/predicate_builder_test.rb- 主キー解決・リレーション生成・predicate builder など、スキーマ情報を多用する部分について、
- Ractor 対応後も今まで通り動作するか
- Ractor ごとの状態差分を持てるか などを確認するテストが追加/更新
- 主キー解決・リレーション生成・predicate builder など、スキーマ情報を多用する部分について、
activesupport/lib/active_support/testing/ractors_assertions.rb- Ractor を使ったテストを支援する assertion 群に、追加アサーションが入っている
- 例: 「複数 Ractor で同じコードを実行しても衝突しない」「Ractor 間で状態が混ざらない」ことを検証する機能
- 影響範囲・注意点
- Ractor を使わないアプリケーション
- 基本的には挙動は変わらない想定(後方互換性を維持するリファクタリング)
- ただし、Active Record の内側の ivar 構造が変わっているため、
- 内部 API(
SchemaContextや内部 ivar)を直接触っている場合は影響しうる
- 内部 API(
- Ractor / マルチスレッドで Active Record を使う場合
- Ractor ごとに独立したスキーマ属性状態を持てるようになり、データ競合や不正なメモリアクセスのリスクが下がる
- モデルのクラスレベル状態(スキーマ由来の ivar)はメイン Ractor で lazy に評価されるため、
- 初回アクセス時にわずかなオーバーヘッドがある一方、
- Ractor 作成時点で全モデルのスキーマを読み込む必要はなくなり、メモリと初期コストを抑えられる
- 内部 API・メタプログラミングに依存しているコード
SchemaContextの具体的な実装(インスタンス変数構成やメソッド参照)に依存している場合、今回の設計変更で壊れる可能性がある- 公開 API(通常の Active Record モデルの使い方)内にとどまっていれば、変更は透過的なはず
- 参考情報 (あれば)
- 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-2文で)
TrilogyAdapterTest でperform_queryをスタブしていたテストが、スタブ後に元のメソッドを正しく戻さずにコネクションプールへ返していたため、非同期クエリのリトライ系テストが期待どおりに失敗を注入できなくなっていました。
このPRでは、スタブ終了後にコネクションインスタンスに残ってしまうperform_queryのシングルトンメソッドをensureブロックで確実に削除し、後続テストへの副作用をなくしています。
- 変更内容の詳細
問題の背景
TrilogyAdapterTest内の3つのテスト#begin_db_transaction raises error#commit_db_transaction raises error#rollback_db_transaction raises error
で、プールから取得したコネクションオブジェクトに対してperform_queryをMinitest#stubで差し替えていました。
Minitest#stubは、対象インスタンスの シングルトンクラスにメソッドを定義する形で stub を差し込む ため、ブロック終了後に「元のメソッドに戻す」処理はしてくれるものの、- シングルトンクラスにエイリアスや元メソッドを残す
- その結果として、そのインスタンスに「クラス定義よりも優先される
perform_queryがぶら下がった状態」
になっていました。
後続の
AsynchronousQueriesTestでは、「perform_queryをアダプタクラスレベルで再定義」することで、意図的にクエリ失敗を発生させ、リトライ動作をテストしています:rubywith_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行追加)。
イメージとしては以下のような変更です(疑似コード):
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 のシングルトンメソッドが確実に消え、クラスレベルの再定義が正しく効くようになります。
- 影響範囲・注意点
- 影響範囲は テストコードのみ であり、本番コード(ActiveRecord Trilogy Adapter 本体)の挙動には変更ありません。
- ただし、これまで「たまたま通っていた」非同期クエリのリトライ周りのテストが、他のテストとの順序依存で落ちる可能性があったものが、順序に依存せず安定して動作するようになります。
- 類似のパターン(インスタンスに対して
stubして、接続プール等に返すもの)が他のアダプタ/テストにもある場合、同様の問題を引き起こす余地があります:- 接続オブジェクトやグローバルに再利用されるオブジェクトに対して
stubを行う場合、- ブロック終了後のメソッド解決順序(シングルトンメソッドの残存)を意識する
- 必要なら
ensureでの後片付けや、クラスレベルの stub/mocking を検討する
ことが推奨されます。
- 接続オブジェクトやグローバルに再利用されるオブジェクトに対して
- 参考情報 (あれば)
- 関連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-2文で)
has_many :throughの関連にスコープをマージする際、すでに through の結合(JOIN)で登場しているテーブルがスコープ側でも再度 JOIN されてしまうケースで、その冗長な JOIN を自動的に削除するようにした変更です。belongs_to/has_one方向の単一レコード JOIN に限って重複を取り除き、クエリ結果が変わり得るコレクション JOIN はそのまま残すようになっています。
- 変更内容の詳細
問題の背景
has_many :through に対して、スコープ内でさらに関連を joins していると、through のチェーンが貼る JOIN とスコープ側の JOIN が同じテーブルを重ねて JOIN してしまうことがありました。
例として PR 説明に出ているケース(#51259由来)を簡略化すると:
class Rubygem < ApplicationRecord
has_many :versions
has_many :dependencies, through: :versions
# このあたりで versions / dependencies を join するスコープを
# incoming_dependencies 的な名前で定義しているイメージ
endhas_many :through の定義と、そのスコープの中身の両方で versions を JOIN してしまうと、生成される SQL に同じテーブルが2回出てきます。
PR前の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.rb の AssociationScope#add_constraints が変更され、以下のロジックが追加されています(要約):
- has_many :through の関連を解決する際に:
- すでに through チェーンの中で JOIN 済みのテーブルに対して、
- マージ対象のスコープ側でも
joinsが指定されている場合、 - その関連が
belongs_toまたはhas_one(= 単一レコードを返す関連)の場合には、- スコープ側の JOIN を「マージ前に」落としてしまう(削除する)。
- 一方、
has_manyなど「コレクションを返す JOIN」は削除しない。
理由: JOIN を1つ減らすと行数が変わる可能性があるため(結合の多重度に影響する)。
結果として、上記の SQL は次のように簡潔になります。
PR後:
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.
つまり:
- スコープ単体で見ると、その
joinsはwhere句内で使っているため必須(消すとActiveRecord::StatementInvalidなどになる)。 - しかし「has_many :through のチェーン経由で同じテーブルが JOIN される場合」に限って、その JOIN は結果として冗長になる。
- この「冗長になるかどうか」の判定は、Active Record が内部の関連解決時にしか把握できないため、アプリケーションコード側で安全に消すのは難しい。
そこで、AssociationScope の中で「through チェーンによる JOIN 情報」と「スコープの JOIN 情報」を突き合わせ、冗長な単一レコードJOINだけを安全に落とすようにしている、という設計です。
- 影響範囲・注意点
- 対象:
has_many :through絡みで、関連のスコープ内にも同じテーブルへのbelongs_to/has_oneJOIN を記述しているコード。- また、そのスコープを
has_many :throughから利用しているケース。
- 期待される影響:
- 発行されるSQLのJOIN数が減り、無駄なJOINがなくなる。
- 結果セット(返るレコード)は変わらない想定。
なぜなら、削除対象は「同じキーで同じテーブルを結合している単一関連JOIN」であり、削除しても条件が変わらないから。
- 意図的に「行数を変えうる」コレクションJOIN(
has_many側)は削除しないため、includesや手動で行っている複雑なコレクションJOINには影響を与えない設計です。 - カスタムArelや、JOINエイリアス名に強く依存した高度なSQLハックをしている場合:
- 内部的に冗長JOINが減ることで、結果的に「エイリアス名が減る」などの副作用が出る可能性はありますが、通常のActive Record APIの範囲では問題は起こりにくいと考えられます。
- 参考情報 (あれば)
- この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-2文で)
Rails の Active Record プリロード処理で、関連の外部キーがnilの場合に余計なクエリが発行されていた問題を修正し、nilキーを正しくスキップするようにした PR です。外部キー/主キーの型が異なり、プリローダがキーを文字列化するケースでも、nilは空文字列に変換せずnilのまま保持します。
- 変更内容の詳細
問題の背景
- Active Record のプリローダ (
ActiveRecord::Associations::Preloader) は、関連付けの外部キーと主キーの型が異なるとき、両方のキーを文字列化してマッチングします。 - その際、オーナー側のキーが
nilだった場合も文字列化され、""(空文字列)になっていました。 - Ruby では空文字列は truthy なので、
owners_by_key(キーごとにオーナーをまとめたハッシュ)に「キー:""」として登録されてしまいます。 - その結果、実際には関連キーが存在しない(
nil)レコードに対してもプリロード用クエリが発行されるが、クエリの結果は当然ヒットせず、関連は最終的にnilになるだけ、という「無駄なクエリ」が発生していました。
今回の修正内容
Preloader::Association#convert_key の挙動を変更:
変更前(概念的なイメージ):
rubydef convert_key(key) key.to_s # nil => "" end変更後(概念的なイメージ):
rubydef convert_key(key) return nil if key.nil? key.to_s end
要点:
- キー変換が必要な場合でも
nilはnilのまま残す。 nil以外のキーは従来通りto_sで文字列化する。
これにより、既存のガードロジック:
owners_by_key[key] # などで「キーが存在するか」を見ている処理において、nil キーを持つオーナーは正しく除外されるようになります(型変換が不要なケースではもともとそうなっていた挙動と揃う)。
テストと関連調整
- 既存の「文字列の外部キーが整数の主キーを指す」関連を用いたリグレッションテストを追加し、
nilの外部キーを持つレコードについて余計なクエリが走らないことを検証。
- 既存の「クエリのグルーピング(条件の異なる 2 クエリになること)」をテストしていたケースは、今までは暗黙的に
nilキー由来の不要なクエリに依存していたため、- 明示的に外部キーを設定して「2 種類の scope のクエリが発行される」ことをテストするように修正。
また、CHANGELOG にバグ修正として追記されています。
- 影響範囲・注意点
- 対象: Active Record の関連プリロード(
includes,preload,eager_load等)で、- 外部キーと主キーの型が異なり、
- プリロー ダ側でキー変換(文字列化)が行われるケース。
- 影響:
- これまで「外部キーが
nilなのにプリロードクエリが飛んでいた」パターンで、余計なクエリが出なくなるため、若干のパフォーマンス改善・無駄な DB 負荷軽減が見込めます。 - 検索結果そのもの(関連が
nilになる/ならない)はもともと正しかったため、アプリケーションの表向きの挙動は基本的に変わりません。
- これまで「外部キーが
- 互換性:
- アプリ側で「なぜか 1 本余計なクエリが走っている」ことに依存していたようなコードがない限り、互換性問題はほぼありません。
- 監視やテストで「発行クエリ数」を厳密に見ている場合は、関連のプリロードでクエリ数が減ることによりテストが変化する可能性があります(N+1 検出用のメトリクスなど)。
- 参考情報 (あれば)
- 対応ファイル:
activerecord/lib/active_record/associations/preloader/association.rbactiverecord/test/cases/associations_test.rbactiverecord/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-2文で)
EventReporter::LogSubscriber#subscription_filterの実装を見直し、イベント名から namespace を抽出するのではなく、namespace から prefix を組み立てる形に変更することで、処理を単純化・最適化した PR です。挙動は従来と同等で、主に内部実装の整理・パフォーマンス改善が目的です。
- 変更内容の詳細
※ 実際の diff は 2 行追加・6 行削除と小規模ですが、概要説明に基づくとロジックの方向性が変わっています。
従来:
- 「イベント名(
"render.action_view"のような文字列)から namespace を切り出す」処理をしていた。 - 例えば
event.name.split(".").lastのような形で"action_view"部分を取り出してフィルタに使っていた、というイメージです。
今回の変更:
- 「namespace(
action_viewなど)から prefix("action_view."のような文字列)を組み立てる」アプローチに変更。 - イベント名 → namespace の抽出処理をなくし、namespace を元に subscription 用の prefix を作ることで、不要な文字列処理を削減しています。
イメージ的なコード変化は以下のようなものです(擬似例・イメージであり実際のコードではありません):
# 変更前(イメージ)
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 のイベントレポート機能に紐づいたログ購読処理なので、この最適化はイベント購読フィルタが多く呼ばれるケースでじわっと効く可能性があります。
- 影響範囲・注意点
外部 API の挙動は原則として変わらない
PR の説明からは、あくまで内部実装の最適化であり、EventReporter::LogSubscriberの public API や利用方法を変えるものではありません。イベント名の構造前提は維持されている
「namespaceから prefix を構築する」ということは、イベント名が"something.namespace"のような形式になっている前提が引き続きあります。
既存の ActiveSupport インストゥルメンテーションの命名規則に乗っていれば問題はありません。独自拡張している場合は注意
EventReporter::LogSubscriber#subscription_filterを直接 monkey patch している、- あるいは EventReporter 周りの内部実装に強く依存したコードを書いている
といったケースでは、この変更を取り込んだ際に挙動を一度確認することをおすすめします。
パフォーマンス面のわずかな改善が期待される
特に大量のイベントが流れる本番環境などで、文字列 split / 解析が減る分、オーバーヘッドが僅かに減ります。ただし変更規模からして「劇的な改善」ではなく、地味な最適化レベルです。
- 参考情報 (あれば)
- 該当クラス:
ActiveSupport::EventReporter::LogSubscriber- ファイル:
activesupport/lib/active_support/event_reporter/log_subscriber.rb
- ファイル:
- 関連ドキュメント:
- ActiveSupport インストゥルメンテーション:
https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html - ログサブスクライバ (LogSubscriber):
https://api.rubyonrails.org/classes/ActiveSupport/LogSubscriber.html
- ActiveSupport インストゥルメンテーション:
この PR はその LogSubscriber の一種である EventReporter 用実装の内部ロジックを整理・最適化するもので、Rails アプリケーションコード側の修正は基本的に不要です。
#58303 Require concurrent before using in ActionCable
マージ日: 2026/9/1 | 作成者: @josevalim
- 概要 (1–2文で)
Action Cable を単体で読み込んだときにクラッシュしていた問題を、必要なconcurrent系モジュールを明示的にrequireすることで解消した PRです。Action Cable を Rails 本体経由ではなく、直接ロードして使うケースでの初期化エラーを防ぐためのバグフィックスです。
- 変更内容の詳細
何が問題だったか
PR の説明にある再現コード:
ruby -Iactivesupport/lib -Iactioncable/lib \
-e 'require "action_cable"; ActionCable.server.executor'のように、Rails 全体ではなく activesupport と actioncable だけを -I で指定して require "action_cable" した場合に、ActionCable.server.executor の呼び出しでクラッシュしていました。
原因は、Action Cable 内部で concurrent-ruby(ActiveSupport::Concurrency / Concurrent::Map など)に依存しているにもかかわらず、Action Cable 単体ロード時にはその依存を自前で require していなかったことです。Rails アプリ全体を require "rails/all" 等で読み込む場合は、他のコンポーネントが concurrent を読み込むため問題が表面化しませんでした。
実際の変更点
変更ファイルは 2 つで、それぞれ 1 行の require が追加されています。
actioncable/lib/action_cable/channel/base.rbactioncable/lib/action_cable/server/base.rb
PR タイトルの「Require concurrent before using in ActionCable」から推測される通り、これらのクラスが concurrent ベースの機能(スレッドセーフなデータ構造や executor 等)を使う前に、明示的に:
require "active_support/concurrent" # もしくはそれに準ずる concurrent 関連ファイルのような行が追加されています。
イメージとしては以下のような差分です(実際のファイル名やパスは PR 依存ですが、概念的にはこのような追加です):
# 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 機能が確実に読み込まれるようになります。
- 影響範囲・注意点
- 影響範囲
- 主に「Action Cable をライブラリとして単体利用する」「最小限の gem ロードで動かす」ような環境に影響します。
- 例:
require "action_cable"だけをして WebSocket サーバ機能を組み込むようなユースケース
- 例:
- Rails フルスタック(
rails newで生成された通常のアプリ)では、元々別コンポーネント経由でconcurrentが読み込まれていたため、挙動はほぼ変わりません。
- 主に「Action Cable をライブラリとして単体利用する」「最小限の gem ロードで動かす」ような環境に影響します。
- 互換性
- 既存機能の挙動を変えるものではなく、「今までクラッシュしていたパスが動くようになる」方向の変更です。
- 追加されたのは
require行のみであり、API 変更や挙動変更はありません。
- パフォーマンス
concurrent-ruby関連のrequireが Action Cable ロード時に確実に走るようになるため、Action Cable 初回ロード時のオーバーヘッドがごくわずかに増えますが、実用上は無視できるレベルです。
- 参考情報 (あれば)
- PR 番号: https://github.com/rails/rails/pull/58303
- 類似の問題は、「コンポーネント単体ロード時に他コンポーネントへの暗黙の依存が崩れる」パターンで発生しがちです。Rails コンポーネントを単体 gem として利用する場合は、「自分のコンポーネント内で使っている外部モジュールは明示的に
requireする」ことを意識しておくと同種のバグを防げます。
#58620 Make some ActionView::Base settings shareable
マージ日: 2026/8/31 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::Baseが持ついくつかの設定値を Ractor 間で共有可能(shareable)にし、Ractor を使ったビュー描画が Ruby 4.0 でも問題なく行えるようにする変更です。クラス変数ベースのcattr_accessorを、シングルトンクラス上のattr_accessorに置き換えることで、Ractor の shareable 制約に対応しています。
- 変更内容の詳細
主な目的
- Ractor 上で
ActionView::Baseを使ってビューをレンダリングできるようにするため、ActionView::Baseの「設定値」を Ractor-shareable な形で保持する。 - 具体的には、以下の設定を Ractor から安全に読める/セットできるようにする:
automatically_disable_submit_tagremove_hidden_field_autocompletedefault_formatsfield_error_proc
実現方法
1) cattr_accessor からシングルトンの attr_accessor への変更
今まで:
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 を利用していた部分を、
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_tagremove_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 テストで保証しています。
- 影響範囲・注意点
既存 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 設計にしている、という位置づけです。
- 参考情報 (あれば)
- Ractors と shareable オブジェクトについて(Ruby 3 以降の仕様)
- Ractor 内で共有できるのは「shareable」なオブジェクトに限られ、基本的に:
- 不変オブジェクト(凍結された文字列、数値、シンボルなど)
- 特定のルールで構成された凍結済みの配列・ハッシュなど
- shareable な Proc(Env が shareable)
- それ以外は Ractor 間に送れない/参照できない。
- Ractor 内で共有できるのは「shareable」なオブジェクトに限られ、基本的に:
- この PR: rails/rails #58620 「Make some ActionView::Base settings shareable」
(ActionView で Ractor 対応を進めるための一環と見なせます)
#58606 Fix encrypted fixtures for JSON columns
マージ日: 2026/8/31 | 作成者: @carlosdanielpohlod
- 概要 (1-2文で)
config.active_record.encryption.encrypt_fixtures = trueを有効にした状態で、json/jsonbカラムをencryptsしている属性のフィクスチャ読込時に発生していた復号エラー(#48601)が、暗号化フィクスチャのシリアライズ処理を修正することで解消されています。暗号化済みペイロードが JSON カラム用に二重シリアライズされていたのを防ぐパッチです。
- 変更内容の詳細
問題の背景
前提:
config.active_record.encryption.encrypt_fixtures = true- モデルで
encrypts :some_attrを宣言 some_attrが DB 上ではjson/jsonbカラムであるケース
これまでの挙動:
EncryptedFixturesがフィクスチャ値を暗号化し、「暗号化済み文字列」をフィクスチャ値として扱う。- フィクスチャを DB に流し込む
build_fixture_sqlが、全カラム値に対してcolumn.typeに応じた型シリアライズ (column.cast_type.serialize) を行う。 json/jsonbカラムの場合:Type::Jsonが値を JSON 文字列にエンコードするため、すでに暗号化済みの文字列がさらに JSON 文字列として包まれる。- 結果として DB には
"{\"p\":...}"のような「JSON 文字列としての暗号文」が保存される。
- レコード読込時、Active Record の復号処理は「JSON オブジェクトの中身」を期待しているのに、実際には「JSON カラムの中に入ったただの文字列」が渡されるため、
ActiveRecord::Encryption::Errors::Decryptionが発生していた。
文字列カラム (
string) はどうだったか:Type::Stringのシリアライズは「文字列をそのまま返す(恒等写像)」なので、暗号化済み文字列をさらに壊さず、そのまま保存されて問題が表面化しなかった。
今回の修正方針
EncryptedFixturesnow hands the payload back in the column's cast form (column.cast_type.deserialize), sobuild_fixture_sqlserializes it once and stores the same JSON object a regular save would.
ポイントは「EncryptedFixtures がフィクスチャ値として返すオブジェクトの型」を、各カラム型の「キャスト済みオブジェクト」に揃えたことです。
変更前(概念的な流れ):
EncryptedFixtures:- フィクスチャの「論理値」 → 暗号化 → 「暗号化済み文字列」をそのまま返す
build_fixture_sql:- 受け取った文字列を
column.cast_type.serializeに通す jsonカラムなら JSON 文字列化され、結果として二重ラップされる
- 受け取った文字列を
変更後(概念的な流れ):
EncryptedFixturesが暗号化ペイロードを、そのカラムのcast_typeに対する deserialize 済み の形にして返す:column.cast_type.deserialize(encrypted_payload)
build_fixture_sqlは、いつも通りcolumn.cast_type.serializeを一度だけ呼ぶ。- その結果、
json/jsonbカラムでは、通常の save と同じ JSON オブジェクト構造が DB に保存されるようになる。
ここでのキモは:
- フィクスチャ → 暗号化 →
column.cast_type.deserialize→build_fixture_sqlでserialize - という 「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 形式で保存されることを確認するテストを追加。
- JSON カラムを
activerecord/test/fixtures/encrypted_book_with_json.yml- 暗号化 JSON カラム用のフィクスチャを追加。
activerecord/test/models/book_encrypted.rb- JSON カラムを持ち
encryptsを宣言したモデルを追加。
- JSON カラムを持ち
activerecord/test/schema/schema.rb- 上記モデル用に JSON カラムをスキーマに追加。
- 影響範囲・注意点
影響を受けるケース:
Active Record Encryptionを使用し、config.active_record.encryption.encrypt_fixtures = trueを有効にしており、encrypts対象の属性が DB 上でjson/jsonbカラムになっている場合。
この PR による挙動の変化:
- これまで:
- 暗号化 JSON カラムのフィクスチャ読み込み時に
ActiveRecord::Encryption::Errors::Decryptionが発生し得た。 - DB 内のデータ形式が
"\"{...}\""のような形になる可能性があった。
- 暗号化 JSON カラムのフィクスチャ読み込み時に
- これから:
- 復号エラーが解消される。
- DB に保存されるデータ形式が「通常の保存時」と同じ JSON 構造になる。
- これまで:
既存データへの影響:
- すでに「二重シリアライズされた状態」で DB に入っているデータは、この修正で自動変換されるわけではありません。
- そうしたデータがあれば、以下のような対応を検討する必要があります:
- データをダンプして再インポートする
- 手動/スクリプトで値を正しい JSON 形式に変換する
- ただし、これは「テスト用フィクスチャ」の利用が主なケースであり、本番 DB に影響することは少ないはずです。
文字列 / バイナリカラム:
- PR の説明にもある通り、
string/binaryカラムは「serialize / deserialize が恒等変換」であるため、挙動は従来と変わりません。
- PR の説明にもある通り、
カスタム型:
json/jsonb以外にも、独自のcast_typeを定義しているカラムがある場合、EncryptedFixtures は「そのカスタム型の deserialize → serialize」のラウンドトリップを通るようになります。- 通常は「通常保存と同じ挙動になる」ので望ましい変更ですが、もし独自型がフィクスチャ処理を前提に特殊な実装をしている場合は挙動が変わる可能性があります(かなりレアケース)。
- 参考情報 (あれば)
関連 Issue:
- #48601 – JSON / JSONB カラム +
encrypt_fixtures = trueでの復号エラー報告
- #48601 – JSON / JSONB カラム +
関連設定:
config.active_record.encryption.encrypt_fixtures = true- テストフィクスチャをロードする際に、暗号化対象属性を自動で暗号化してから DB に保存するための設定。
実務的な確認ポイント:
- 暗号化 JSON カラムを使っている場合、Rails をこの修正を含むバージョンに上げた後、次を確認すると安全です:
- フィクスチャロード(
rails test,rails db:fixtures:load等)がエラーなく完走すること - DB に保存されている JSON カラムのフォーマットが期待どおりであること(
"{\"p\":...}"のような「さらにクォートされた文字列」になっていないこと)
- フィクスチャロード(
- 暗号化 JSON カラムを使っている場合、Rails をこの修正を含むバージョンに上げた後、次を確認すると安全です:
#58612 Fix NameError when ActiveRecord::Coders::JSON is given encode options
マージ日: 2026/8/31 | 作成者: @lazerg
- 概要 (1-2文で)
ActiveRecord のActiveRecord::Coders::JSONにencode_optionsを渡したときにNameErrorが発生していた回 regresson を修正した PRです。イニシャライザ引数名のリネーム漏れによるバグを直し、encode オプション付きの利用を担保するテストが追加されています。
- 変更内容の詳細
バグの内容
以下のようなコードを書くと:
coder = ActiveRecord::Coders::JSON.new(encode_options: { escape: true })実行時に:
NameError: undefined local variable or method `options'というエラーが発生していました。
原因は、以前の PR #58601 で ActiveRecord::Coders::JSON のイニシャライザ引数が
def initialize(options = {})のような形から、キーワード引数 encode_options: に変更された一方で、内部でデフォルト値とマージするコードがまだ options という古い変数名を参照していたためです。
イメージとしては、元がこんな感じだったのに対し:
def initialize(encode_options: {})
@encode_options = DEFAULT_ENCODE_OPTIONS.merge(options) # ← ここが古い名前
endoptions というローカル変数が存在しないため NameError になっていました。
修正内容
この PR では、そのマージ処理を正しく encode_options に合わせるように 1 行だけ修正しています。
@encode_options = DEFAULT_ENCODE_OPTIONS.merge(encode_options)これにより、以下のような「デフォルトの JSON エンコードオプションに対して、呼び出し側で上書きする」という本来の挙動が復活します。
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: {...})が例外を出さずに初期化できること- そのオプションが内部に反映されていること
を確認するものです。
- 影響範囲・注意点
- 対象ブランチ:
mainおよび8-1-stableにのみ存在する regression であり、まだリリース版には入っていないため、現時点の正式リリースを使っているアプリには影響しません。 - 影響範囲:
ActiveRecord::Coders::JSONを 直接 使い、かつencode_options:キーワード引数を渡しているコードが対象です。- Rails 内部でこのクラスを使っている箇所については、encode_options を明示的に指定していなければ影響はありません。
- 挙動の変化:
- 既存の挙動(デフォルトオプション+呼び出し側オプションのマージ)が本来のとおりに復活するだけで、API 仕様の変更や互換性破壊はありません。
- 注意点:
- まだ未リリースのため CHANGELOG には記載されていません。
main/8-1-stableを追いかけている場合は、この PR を取り込むか、該当期間のコミットで回避されているか確認する必要があります。 - 自前で monkey patch している場合は、この修正と二重にパッチを当てていないか確認してください。
- まだ未リリースのため CHANGELOG には記載されていません。
- 参考情報 (あれば)
- 該当 Issue: #58609 (
ActiveRecord::Coders::JSON.new(encode_options: ...)でのNameError報告) - 関連 PR: #58601 (
options→encode_optionsへのイニシャライザ変更を行った PR。今回の regression の原因)