Ruby on Rails PR Digest - 2026年 7月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#57825 Introduce ractorize
マージ日: 2026/7/6 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails アプリケーションを Ractor から共有可能にするための土台として、Rails::Application#ractorize!が導入され、基本的な Rails アプリをRactor.make_shareableできるようにする PR です。あわせて、Ractor 非対応だったいくつかの内部実装(継承オプション、KeyGenerator、Mutex など)が Ractor セーフになるよう調整されています。
- 変更内容の詳細
新機能: Rails::Application#ractorize!
Rails::Application に以下のメソッドが追加されています。
class MyApp::Application < Rails::Application
config.eager_load = true
config.cache_store = :null_store
# Ractor から共有可能にする
config.after_initialize do
Rails.application.ractorize!
end
endractorize! が行うこと(PR 説明+差分から推測される内容):
- アプリケーションを Ractor 共有可能な状態に「準備」する
- Ractor セーフでないオブジェクト(Mutex など)を排除・解体
- Engine / Initializer 周りの内部状態を Ractor 共有可能な形に再編
- 最後に
Ractor.make_shareable(app)を呼び出し、Rails.application自体を Ractor から共有可能なオブジェクトにする
これにより、例えば「メイン Ractor でアプリをブート → サブ Ractor にアプリを共有 → 各 Ractor でリクエスト処理」みたいな構成の土台が整えられます。
制約事項
現時点(この PR 時点)で ractorize! を使うためには、次の条件が必要です:
- アプリケーションコードとルーティングは eager load 必須
config.eager_load = true- 遅延ロードだと、Ractor 間でクラス定義のタイミングや状態が不整合になるため
- Action Cable は非対応
- 内部的にスレッド/コネクション管理など Ractor セーフでない構造が多数あるため
- キャッシュストアは
:null_store必須- 他のキャッシュストア(メモリストアなど)が Ractor 共有できないデータ構造を使っているため
将来的にはこれらの制約を減らす予定とのことですが、この PR はあくまで最初の足場です。
継承オプション (Rails::Configuration / Rails::Engine / Rails::Initializable)
Rails::Configuration や Engine の継承オプション、initializer 系の設定は、これまで「通常の Ruby オブジェクト(しばしばミュータブル)」を前提にしていました。
この PR では:
- 「継承オプション」の保持方法が Ractor セーフになるよう調整
Hashのdefault_procがミューテーションを行わないようにしたり- 初期化後に freeze 可能な形に揃えたり
Rails::Engineごとにアプリケーションを eager に構築し、Ractor 非共有なオブジェクト(特に Mutex)を「初期化時に使って、共有前に破棄」するようなフローに変更Rails::Initializable周りで使われるProcなどを Ractor 共有可能な形に修正
これにより、Rails.application の内部構成(設定や初期化処理の一覧など)を Ractor.make_shareable に掛けられるようになります。
KeyGenerator を Ractor セーフに
ActiveSupport::KeyGenerator と、その利用箇所である ActiveSupport::Messages::RotationCoordinator に変更が入っています。
ポイント:
- これまで
KeyGeneratorは内部で「Concurrent Map(あるいは似た構造)」を使っており、これは Ractor 共有不可能でした。 - この PR では:
- 「キー生成用のキャッシュ(Map)」を Ractor ローカル にする設計に変更
- つまり「Ractor ごとに KeyGenerator のキャッシュを持つ」ようにして、
Rails.application自体は共有しつつも、共有不可能な Map は各 Ractor 側で持つ
影響として:
- 同じ
Rails.applicationを複数 Ractor から使っても、KeyGenerator自体は共有されるが、その内部キャッシュは各 Ractor で独立して持たれる。 - そのため、初回アクセス時のキー計算コストは Ractor ごとに一度ずつ発生するが、Ractor セーフネスとトレードオフにした pragmatic な対応になっている。
テスト (activesupport/test/key_generator_test.rb) も追加されており、Ractor 使用時の振る舞いを検証しています。
ActionDispatch Journey / Transition Table の Ractor 対応
actionpack/lib/action_dispatch/journey/gtg/transition_table.rb に追記:
- ルーティングエンジン(Journey)の状態遷移テーブル構築処理で、Ractor 共有可能でない要素(例: デフォルト Proc 付き Hash)が含まれていた部分を修正
- 遷移テーブル構造が
Ractor.make_shareableできるようにするための小変更(ミュータブルな default_proc の削除・freeze など)が行われていると考えられます。
これにより、Rails.application.routes の内部表現も Ractor 共有しやすくなります。
Ractor 関連テストの追加
activesupport/lib/active_support/testing/ractors_assertions.rb- Ractor 関連のアサーションヘルパを定義
- 例: 「このオブジェクトは Ractor から共有できるか」などをテストで確認する仕組み
railties/test/application/ractors_test.rb- 実際に Rails アプリをブートして
ractorize!を呼び、別 Ractor から利用できるかなどを検証する統合テスト - eager load / null_store などの前提条件を満たしたアプリで、
Ractor.make_shareableが成功することを確認していると推測できます。
- 実際に Rails アプリをブートして
- 影響範囲・注意点
影響範囲
- Rails 内部:
- アプリケーション設定 (
Rails::Configuration) - Engine の構造 (
Rails::Engine) - Initializer の登録・実行方法
- KeyGenerator / メッセージ暗号化系
- ルーティングエンジン(Journey)の内部構造
- アプリケーション設定 (
- 外部 API レベル:
- 新メソッド
Rails::Application#ractorize! - Ractor ベースで Rails を動かしたい人向けの初期サポート
- 新メソッド
既存アプリへの影響
- この PR 自体は、既存アプリが従来どおり(Ractor を使わずに)動く場合には、基本的に挙動変更を引き起こさないように配慮されています。
- ただし以下の点は意識しておくと良いです:
- internal な Hash の default_proc など、一部挙動が微妙に変わっている可能性はある(通常アプリではまず問題にならないレベルを目指しているはず)
- KeyGenerator の実装変更に伴い、Ractor を明示的に使う場合は「Ractor ごとに KeyGenerator のキャッシュが別になる」ことを理解しておく必要がある
Ractor を使いたい場合の注意点
- 必須条件:
config.eager_load = trueconfig.cache_store = :null_store- Action Cable を使わない
- 実運用で使うには、まだ制約が大きく、かつスレッド・プロセス・Ractor の組み合わせの設計が難しいため、現時点では「実験的」な段階と見なすのが妥当です。
- アプリケーションコード側も、自前で保持するグローバル状態やシングルトン、クラス変数などが Ractor セーフになっていないと、
ractorize!後のRactor.make_shareableに失敗する可能性があります。
- 参考情報 (あれば)
- この PR で触れられている関連 PR:
- Inheritable options の Ractor 対応に関する議論: https://github.com/rails/rails/pull/57757
- Ractor 自体の仕様:
- Ruby 公式ドキュメント「Ractor」: https://docs.ruby-lang.org/ja/latest/doc/ractor_md.html
- Ractor 対応の設計上の方針:
- 共有可能なオブジェクト (
Ractor.shareable?) のみを共有し、Ractor ローカルで持つべきもの(キャッシュや Mutex など)は Ractor ごとに分離する、という設計が Rails 内部にも適用され始めているのがこの PR のポイントです。
- 共有可能なオブジェクト (
#58018 Translate if_exists/if_not_exists guards when inverting remove_column and add_reference/remove_reference
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
remove_columnおよびadd_reference/remove_reference(とそのエイリアスadd_belongs_to/remove_belongs_to)について、マイグレーションの巻き戻し時にif_exists/if_not_existsオプションが正しく反転して適用されるように修正した PR です。これにより、これらのコマンドを使った冪等なマイグレーションのchange/revertが、期待通りに動作するようになります。
- 変更内容の詳細
背景: command_recorder と idempotency guard
Rails の ActiveRecord::Migration::CommandRecorder は、change マイグレーションや revert ブロック実行時に発行されたコマンドを記録し、その逆操作(inverse)を自動生成します。
既に以下のコマンドでは、if_exists と if_not_exists が「逆向きのガード」として正しく変換されていました:
add_columnadd_indexadd_foreign_key/remove_foreign_keyadd_check_constraint/remove_check_constraint
しかし、以下のコマンドファミリーではこの変換が抜けていました:
remove_columnadd_reference/remove_referenceadd_belongs_to/remove_belongs_to(add_reference/remove_referenceのエイリアス)
今回の PR は、この「抜けていた2ファミリー」に対してもガードの変換を適用するものです。
remove_column の変更
変更前の挙動
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_exists: true }], nil]remove_columnに渡したif_exists: trueが、そのまま逆操作のadd_columnにも渡されていました。- しかし、
add_columnはif_existsオプションを認識しないため、ロールバック時にはガードが完全に無視され、「意図に反する形で」動いてしまう可能性がありました。- 元々の意図: 「カラムが存在する場合だけ remove する」→ ロールバックでは「カラムが存在しない場合だけ add する」としたい
- 実際の挙動: ロールバック側でガードが消えるため、常に
add_columnを実行してしまう
変更後の挙動
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_not_exists: true }], nil]remove_column(if_exists: true)の逆操作はadd_column(if_not_exists: true)となり、- 削除側: 「あれば消す」
- 逆操作: 「なければ作る」
- というペアになり、冪等な前方・後方マイグレーションが成立するようになりました。
add_reference / remove_reference(およびエイリアス)の変更
変更前の挙動
inverse_of :add_reference, [:t, :u, if_not_exists: true]
# => [:remove_reference, [:t, :u, { if_not_exists: true }]] # ガードが変換されず無視される
inverse_of :remove_reference, [:t, :u, if_exists: true]
# => [:add_reference, [:t, :u, { if_exists: true }]] # 同上- コマンド名だけが
add↔removeで入れ替わり、オプションハッシュはそのまま渡されていました。 - その結果:
add_reference側はif_not_existsをサポートしていてもremove_reference側はif_not_exists/if_existsをサポートしていなかったり、逆にif_existsだけを見ていたり
- という非対称な API のせいで、片方のコマンドで指定したオプションが逆操作側で「無視」されていました。
変更後の挙動
add_reference(if_not_exists: true)の逆操作はremove_reference(if_exists: true)remove_reference(if_exists: true)の逆操作はadd_reference(if_not_exists: true)- となるように、
if_exists/if_not_existsを互いに反転して渡すように修正されました。
# 例: change マイグレーション
def change
add_reference :comments, :post, if_not_exists: true
end
# ロールバック時に内部的に解釈されるイメージ
# => remove_reference :comments, :post, if_exists: trueadd_belongs_to / remove_belongs_to は add_reference / remove_reference のエイリアスなので、この修正の恩恵をそのまま受けます。
- 影響範囲・注意点
- 対象:
changeマイグレーション(またはrevertブロック)でremove_column(..., if_exists: true)add_reference(..., if_not_exists: true)remove_reference(..., if_exists: true)- およびその
*_belongs_toエイリアス を使っているケース。
- 影響:
- これまで:
- 逆操作側でガードが「消える / 無視される」ことがあった。
- そのため、本番環境などで「途中の状態からロールバック」するようなケースで、不要なエラーや意図しない再作成などが起こりうる。
- これから:
if_exists/if_not_existsが逆操作側にも正しく反映され、前方・後方どちらの方向にも冪等性が担保されやすくなる。
- これまで:
- 互換性について:
- 挙動変更ではありますが、「ガードが無視されていた」状態から「意図通りガードされる」状態への修正なので、ほとんどの場合はバグ修正として望ましい変更です。
- もし「逆操作側でガードが付かないこと」を前提にした非常に特殊なワークアラウンドをしている場合は、今回の修正で挙動が変わる可能性があります(そのような依存は避けるべきです)。
- 参考情報 (あれば)
- 変更ファイル:
activerecord/lib/active_record/migration/command_recorder.rbinverse_ofのロジックに、remove_columnおよびadd_reference/remove_reference系に対するif_exists↔if_not_existsの変換処理を追加。
activerecord/test/cases/migration/command_recorder_test.rb- 上記の変換が正しく行われることを保証するテストを追加。
- 関連する既存仕様:
- すでに
add_column,add_index,add_foreign_key/remove_foreign_key,add_check_constraint/remove_check_constraintでは、同様の guard 変換が行われており、本 PR はその不整合を解消するものです。
- すでに
#58024 Derive the expected queue from the delivery job in assert_enqueued_email_with
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
assert_enqueued_email_withが、メール送信時のキュー名を「メイラー側の設定」ではなく「delivery_job 側の設定」から導出するように修正されました。これにより、カスタムdelivery_jobでqueue_asを指定している場合でも、正しくテストが通るようになります。
- 変更内容の詳細
これまでの挙動(問題点)
ActionMailer::TestHelper#assert_enqueued_email_with は、期待されるキュー名を mailer_class.deliver_later_queue_name から導出していました。
しかし、次のようにカスタム delivery_job を使い、そのジョブに queue_as を設定しているケースでは問題が発生していました。
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
queue_as :notifications
end
class RegistrationMailer < ApplicationMailer
self.delivery_job = RegistrationDeliveryJob
end
RegistrationMailer.welcome.deliver_later
assert_enqueued_email_with RegistrationMailer, :welcome
# => No enqueued job found with {job: RegistrationDeliveryJob, ..., queue: "mailers"}
# Potential matches: {..., queue: "notifications", ...}- 実際には
queue: "notifications"にジョブが積まれている - しかしアサーションは
queue: "mailers"を期待してしまうため、失敗していました
これは deliver_later のドキュメント上「カスタム delivery_job を使う場合は、その job がキュー名を支配する」とある契約と食い違っていました。
今回の修正内容
変更ファイル:
actionmailer/lib/action_mailer/test_helper.rb(+1 / -1)actionmailer/test/test_helper_test.rb(+18 / 0)
テストヘルパの実装を微調整し、「期待されるキュー名の決定ロジック」を以下のように変更しています。
- 以前:
mailer_class.deliver_later_queue_nameからキュー名を導出 - 今回:
mailer_class.delivery_job側からキュー名を導出
→delivery_jobがqueue_asで指定したキューがある場合はそれを使う
結果として:
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
queue_as :notifications
end
class RegistrationMailer < ApplicationMailer
self.delivery_job = RegistrationDeliveryJob
end
RegistrationMailer.welcome.deliver_later
assert_enqueued_email_with RegistrationMailer, :welcome
# 期待キュー名が "notifications" になり、正常にパスするテスト (test_helper_test.rb) には、この振る舞いを保証するためのケースが追加されています(カスタム delivery_job + queue_as を使った場合のアサーション成功を検証)。
なお、デフォルトの MailDeliveryJob を使うケースや queue_name_prefix を利用しているケースでも、最終的に解決されるキュー名は従来と変わらないように設計されています。
- 影響範囲・注意点
テストでの影響
- カスタム
delivery_jobを使っていてqueue_asを指定している場合、これまで不当に落ちていたassert_enqueued_email_withが通るようになります。 - 逆に言えば、「実運用ではカスタム delivery_job のキューに積まれている」のに、「テストで mailer 側のキュー名を前提にしていた」ような間違ったテストは、今回の修正で正しい挙動に修正されます。
- カスタム
既存コードへの互換性
- デフォルトの
MailDeliveryJobを使っている場合、これまでとキュー名の解決結果は同じになるように配慮されているため、通常は影響はありません。 queue_name_prefixを利用している場合も、最終的なキュー名は従来と同じになるように実装されています。
- デフォルトの
テストコード側で明示的に queue を指定している場合の確認
assert_enqueued_email_withにqueue:オプションで明示的にキューを指定している場合、その指定がカスタムdelivery_jobの設定と矛盾していないかは確認しておくと安全です。
- 参考情報 (あれば)
ActionMailer::TestHelper#assert_enqueued_email_with:- メイラーとメソッドを指定して、そのメールが正しくジョブとしてエンキューされているかを検証するためのテストヘルパ。
ActionMailer::Base#delivery_job:- メール送信に使用する Active Job クラスを指定するための設定。
deliver_laterのドキュメント上、「カスタム delivery_job を設定した場合、その job がキュー名 (queue_as) を決定する」と規定されている。
- 関連 PR: #58009(今回の修正はそのフォローアップとして位置付けられています)。
#58017 Preserve subsecond precision in the DateTime.now travel stub under with_usec
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
travel_to(time, with_usec: true)使用時に、DateTime.nowでもサブ秒(マイクロ秒)精度が正しく保持されるように修正したPRです。これにより、同じ凍結時刻を参照するTime.nowとDateTime.nowの挙動が一致します。
- 変更内容の詳細
何が問題だったか
ActiveSupport::Testing::TimeHelpers#travel_to には with_usec: オプションがあります。
with_usec: true
凍結する時刻のサブ秒(usec)まで含めて時間を固定する意図。with_usec: false(デフォルト)
秒単位で丸めて時間を固定する(usecは 0 になる)。
これまでは:
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
Time.now.usec # => 999999 # サブ秒が保持される
DateTime.now.usec # => 0 # サブ秒が失われる(バグ)
endと、Time.now は with_usec: true に従ってサブ秒を保持していた一方、DateTime.now のスタブは秒単位に切り捨ててしまっていました。
今回の修正内容
activesupport/lib/active_support/testing/time_helpers.rb で DateTime.now のスタブ生成部分が 1行変更され、with_usec: true のときにサブ秒を保持するようになりました。
PRの説明に対応する挙動は次の通りです:
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
Time.now.usec # => 999999
DateTime.now.usec # => 999999 # Time.now と一致するように修正
endまた、テスト (activesupport/test/time_travel_test.rb) に DateTime.now の usec を検証するケースが 13行追加されています。
これにより、with_usec: true で DateTime.now がサブ秒を保持すること、with_usec: false では保持しないこと(usec == 0)が回 regresion テストとして担保されます。
with_usec: false の挙動は変わらない
デフォルトの with_usec: false の場合は、これまで通り DateTime.now.usec は 0 のままです:
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do # with_usec: false がデフォルト
Time.now.usec # => 0
DateTime.now.usec # => 0
end- 影響範囲・注意点
影響範囲:
- テストコードで
ActiveSupport::Testing::TimeHelpers#travel_toとDateTime.nowを併用しており、かつwith_usec: trueを指定しているケース。 - そういったコードでは、これまで「秒単位に丸められていた」
DateTime.nowが「マイクロ秒まで含めて凍結される」ように変わります。
- テストコードで
挙動変化により起こりうること:
- 以前の動作に依存して「
DateTime.now.usecは常に 0 である」と決め打ちしていたテストがあると、with_usec: trueの文脈では失敗する可能性があります。 - ただし、
with_usec: trueを明示している場合は「サブ秒を保持したい」という意図が通常のため、この変更は意図により沿ったものといえます。
- 以前の動作に依存して「
注意点:
- 秒単位で固定したいテストは、
with_usec:を明示的に指定するか、with_usec: falseを使うことを前提にするとよいです。 - ライブラリやアプリ側で、
DateTime.nowベースのロジックとTime.nowベースのロジックを混在させていた場合、これにより両者の挙動が揃うため、むしろ一貫性が向上します。
- 秒単位で固定したいテストは、
- 参考情報 (あれば)
- 該当コード:
ActiveSupport::Testing::TimeHelpers#travel_to(time_helpers.rb)
- 挙動の意図:
with_usec: true… テスト中の「凍結時刻」をサブ秒精度まで含めて再現したい場面向け。with_usec: false… 秒単位で十分なテスト向け(以前からのデフォルト挙動)。
#58021 Honor an explicit :selected option in time_zone_select
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
time_zone_selectヘルパーにselected:オプションを明示的に渡した場合、その値がきちんと選択状態として反映されるように修正したPRです。selected:を指定しない既存コードの挙動は変わりません。
- 変更内容の詳細
これまでの問題
time_zone_select は第4引数のオプションとして selected: を受け取れますが、実際には無視されており、常に「オブジェクトの属性値」もしくは「default: オプション」で選択タイムゾーンが決まっていました。
time_zone_select("firm", "time_zone", nil, selected: "B")
# 本来は "B" を選択させたいが、
# 実際には firm.time_zone(または default:)の値が選択されていたフォームオブジェクト側の値を一時的に上書きしたいケース(例: 管理画面で一括編集、確認画面で一時的に別の値を見せたい等)で、selected: が効かないというバグ的挙動になっていました。
修正後の挙動
このPRでは time_zone_select の内部で、「どの値を選択状態にするか」の決定ロジックにおいて、:selected が明示的に渡されていればそれを最優先で使うように修正しています。
- 優先順位(概念的には):
options[:selected]があればそれを選択値とする- なければ、従来通り「オブジェクトの属性値」または
options[:default]を使う
使用イメージ:
# 明示的に "B" を選択したい
time_zone_select("firm", "time_zone", nil, selected: "B")
# => HTML上も "B" に対応する <option> が selected になる
# これまで通りオブジェクトの値を使いたい場合
time_zone_select("firm", "time_zone")
# => firm.time_zone が selected
# オブジェクトに値がなければ default: を使う(従来通り)
time_zone_select("firm", "time_zone", nil, default: "UTC")コード上の変更点としては、ActionView::Helpers::Tags::TimeZoneSelect 内の選択値決定ロジックに 3行程度の追加があり、テスト (form_options_helper_test.rb) で「selected: を渡したときにそれが優先される」ことを検証するケースが追加されています。
- 影響範囲・注意点
影響を受けるヘルパー
ActionView::Helpers::FormOptionsHelper#time_zone_selectform_for/form_with等から呼ばれる同名ヘルパーも含む
既存コードへの影響
selected:を渡していないコードは挙動が変わりません(オブジェクト値 +default:の従来ロジックそのまま)。- これまで「
selected:を指定しても無視される」と思ってワークアラウンドをしていた場合は挙動が変わる可能性があります。
例えば:rubyこうしたコードがあると、今回から# selected: が無視される前提で、わざと「無関係な値」を渡していたような特殊なハック time_zone_select("user", "time_zone", nil, selected: "dummy")"dummy"(に対応するタイムゾーン)が本当に選択されてしまいます。
注意点 / ベストプラクティス
- 「モデルの値をそのまま使いたい」場合は
selected:を指定しない方が分かりやすく安全です。 - 「モデルの値よりも、フォーム側で明示的に指定した選択状態を優先したい」ケース(例: 確認画面での上書き、独自の初期値制御)では
selected:を積極的に使えるようになります。 default:はあくまで「オブジェクトの値がない時のフォールバック」として機能し、selected:を指定すればdefault:よりもselected:が優先されます。
- 「モデルの値をそのまま使いたい」場合は
- 参考情報 (あれば)
- 対象コード:
actionview/lib/action_view/helpers/tags/time_zone_select.rb - テスト:
actionview/test/template/form_options_helper_test.rb - 関連ヘルパー:
select,time_selectなど他の select 系ヘルパー も似た API を持つため、今後selected:の扱いがそれらと整合的になるかを見る際の参考になります。
#58012 Fix broken links in guides [ci skip]
マージ日: 2026/7/6 | 作成者: @fuentesjr
- 概要 (1-2文で)
このPRは、Rails Guides 内および外部サイトへの「リンク切れ」を3箇所修正するドキュメント専用の変更です。動作コードには一切手を入れておらず、ガイドの参照先を正しいURL/アンカーに差し替えています。
- 変更内容の詳細
変更はいずれも1行ずつのリンク先修正です。
2-1. Active Storage ガイド: ImageMagick のリンク修正
対象: guides/source/active_storage_overview.md の Requirements セクション
変更前:
https://imagemagick.org/index.phpこの URL は 404 になるため、サイトルートに変更。
変更後:
https://imagemagick.org/Rails 自体の機能には影響しませんが、Active Storage をセットアップする際に参照する ImageMagick の公式サイトへのリンクが正常に開けるようになります。
2-2. Action Mailer ガイド: Active Job へのアンカー修正
対象: guides/source/action_mailer_basics.md の「コントローラの編集」付近
Action Mailer から Active Job を使ってメールを非同期送信する (deliver_later) 説明へのリンクが壊れていました。
変更前:
active_job_basics.html#action-mailerActive Job ガイド側に #action-mailer というアンカーは存在せず、クリックしてもページトップ(もしくは何も起きない)状態でした。
変更後:
active_job_basics.html#example-sending-email#example-sending-email セクションは、Active Job 経由でのメール送信(deliver_later など)を具体的に解説している箇所であり、文脈的にも正しいターゲットです。
Action Mailer ガイドから「メールをジョブ経由で送るサンプル」に直接ジャンプできるようになります。
2-3. Configuring ガイド: キャッシュストアへのアンカー修正
対象: guides/source/configuring.md の config.cache_store の説明部分
config.cache_store の説明中で、キャッシュストアの詳細として Caching ガイド中の特定セクションへリンクしていますが、そのアンカー名が存在しませんでした。
変更前:
caching_with_rails.html#cache-storesCaching ガイドには #cache-stores セクションがなく、リンクはページトップ相当になっていました。
変更後:
caching_with_rails.html#other-cache-stores#other-cache-stores セクションは、config.cache_store の説明で列挙されているストアと一致する内容を扱っています:
:memory_store:file_store:mem_cache_store:null_store:redis_cache_store
これにより、config.cache_store の説明から、そのまま各キャッシュストアの詳細説明へジャンプできるようになります。
- 影響範囲・注意点
- 影響範囲は Rails Guides (ドキュメント) のみで、アプリケーションコードやフレームワークの挙動には一切影響しません。
- 既存の URL あるいはアンカーから別の場所へ「リダイレクト」されるようになったのではなく、もともと機能していなかったリンクを、意図した場所に正しく張り直した変更です。
- ドキュメント生成 (guides のビルド) が通るかどうかはほぼノーリスクですが、
- プレーンな URL (
https://imagemagick.org/) - 同一リポジトリ内の他のガイドへの相対リンク (
active_job_basics.html#example-sending-email,caching_with_rails.html#other-cache-stores)
の3つなので、ビルドエラー等の可能性も実質ありません。
- プレーンな URL (
- リンクターゲットとなるガイド側のアンカー名 (
#example-sending-email,#other-cache-stores) が今後変更されると再びリンク切れになるため、ドキュメント側でアンカーを変更する際は相互参照の確認が必要です。
- 参考情報 (あれば)
- 該当ガイド(edge guides):
- Active Storage Overview:
https://edgeguides.rubyonrails.org/active_storage_overview.html#requirements - Action Mailer Basics:
https://edgeguides.rubyonrails.org/action_mailer_basics.html#edit-the-controller - Active Job Basics(修正先アンカー):
https://edgeguides.rubyonrails.org/active_job_basics.html#example-sending-email - Configuring Rails Applications (
config.cache_store):
https://edgeguides.rubyonrails.org/configuring.html#config-cache-store - Caching with Rails(修正先アンカー):
https://edgeguides.rubyonrails.org/caching_with_rails.html#other-cache-stores
- Active Storage Overview:
運用面では、こうした broken link は CI で自動検出しづらいことが多いため、今後リンクチェッカー(外部URL & 内部アンカー)の導入を検討すると、同種の不具合を早期に防ぎやすくなります。
#58016 Forward keyword arguments through deprecation proxies
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
DeprecatedObjectProxy/DeprecatedInstanceVariableProxy経由でキーワード引数必須メソッドを呼ぶとArgumentErrorになっていた問題を修正し、正しくキーワード引数として転送されるようにした PR です。Ruby 3 以降の「キーワード引数の分離」による不具合を、ruby2_keywordsを使って最小限の変更で解消しています。
- 変更内容の詳細
何が問題だったか
Ruby 3 以降では、以下のような「最後の Hash をキーワード引数として解釈する」挙動が変わり、*args で受けるとキーワードにならず「位置引数の Hash」として扱われます。
def target(a:)
a
end
def caller(*args, &block)
target(*args, &block)
end
caller({ a: 1 })
# Ruby 3 では ArgumentError: wrong number of arguments ...ActiveSupport::Deprecation の以下のプロキシクラスが、まさにこのパターンでした。
ActiveSupport::Deprecation::DeprecatedObjectProxyActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
これらはベースクラス DeprecationProxy#method_missing を継承しており、その定義は
def method_missing(method, *args, &block)
warn(method, caller)
@object.__send__(method, *args, &block)
endのような形でした。このため、プロキシ対象のメソッドが「必須キーワード引数」を持っていると、以下のようなエラーになっていました。
class Sample
def foo(a:)
a
end
end
sample = Sample.new
proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(sample, "sample", ActiveSupport::Deprecation.new)
proxy.foo(a: 1)
# Before:
# ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: a)Hash { a: 1 } が位置引数として転送されてしまい、キーワードとして解釈されなかったためです。
どう直したか
DeprecationProxy#method_missing に ruby2_keywords を付けました。
class DeprecationProxy
ruby2_keywords def method_missing(method, *args, &block)
warn(method, caller)
@object.__send__(method, *args, &block)
end
endruby2_keywords を付与すると、
- 呼び出し元での「キーワードっぽい Hash」としての情報が
*argsの中にフラグ付きで保持される - 呼び出し先(ここでは
@object.__send__)に転送したとき、再び「キーワード引数」として解釈される
という Ruby 2 互換の挙動が有効になります。
これにより、先ほどの例は次のように期待どおり動作します。
proxy.foo(a: 1)
# After:
# => 1
# かつ deprecation warning は従来どおり出るなぜ *args, **kwargs にしなかったのか
DeprecationProxy#method_missing は warn に args を渡し、特に DeprecatedInstanceVariableProxy#warn では args.inspect をそのままメッセージに埋め込んでいます。
def warn(method, callstack)
ActiveSupport::Deprecation.warn(
"some message... Args: #{args.inspect}",
callstack
)
endここでメソッドシグネチャを
def method_missing(method, *args, **kwargs, &block)
...
endのように分割してしまうと、args からキーワードが抜け落ち、警告メッセージ上では
- 以前:
Args: [1, {:a=>2}] - 変更後:
Args: [1](キーワードは別のkwargsにいる)
となり、互換性が崩れます。
ruby2_keywords を使えば、
- 呼び出し側・メッセージ上では「旧来どおり
argsに Hash が入っている」状態を維持 - 転送先メソッドでは Ruby 3 スタイルのキーワードとして正しく解釈
という両方を満たせるため、この一行だけで済ませています。
他との一貫性
同じファイルにある DeprecatedConstantProxy#method_missing は、すでに
def method_missing(...)
...
target.__send__(...)
endという「...(dot-dot-dot)」構文でキーワードを正しく転送する形に直されていました。この PR は、その sibling 実装と挙動を揃える修正でもあります。
テスト追加
activesupport/test/deprecation_test.rb にテストが追加され、以下のようなケースがカバーされています(要約):
DeprecatedObjectProxy経由で、必須キーワード引数を持つメソッドを呼べることDeprecatedInstanceVariableProxy経由でも同様に動作すること- その際に deprecation warning が出ること
- 引数の表示(
args.inspect)が従来どおりであること
- 影響範囲・注意点
影響範囲
ActiveSupport::Deprecation::DeprecatedObjectProxyActiveSupport::Deprecation::DeprecatedInstanceVariableProxy- 特に、キーワード引数を持つメソッドをこれらのプロキシ越しに呼んでいるコードは挙動が変わります(以前は
ArgumentErrorで落ちていたが、正常に呼べるようになる)。
後方互換性
- これまで例外が出ていた箇所が正しく動作するようになるため、基本的にはバグフィックスです。
warnに渡されるargsの内容・inspect文字列は維持されるので、ログ監視・テストでメッセージを厳密比較している場合でも影響はほぼありません。ruby2_keywords自体は Ruby 3 系では「非推奨だがまだ使える互換機能」として残っていますが、Rails 内部実装に閉じており、利用者側のコードに直ちに影響はありません。
注意点
- 将来 Ruby 本体から
ruby2_keywordsが削除される可能性はあります。その場合、この周辺実装を*args, **kwargsなどに書き換えつつ、警告メッセージの互換性をどう担保するか、という課題は残ります。 - プロキシ経由でのメソッド呼び出しに頼ったテストが、「例外が出ること」を前提にしていた場合は、テストが通らなくなる可能性があります。
- 将来 Ruby 本体から
- 参考情報 (あれば)
- Ruby のキーワード引数分離に関する変更点(Ruby 3.0)
ruby2_keywordsの挙動について(Ruby リファレンス)- 類似の修正(
DeprecatedConstantProxyのキーワード転送)- コミット
946e46ebcc(Rails 本体リポジトリ内)
- コミット
#58015 Strip surrounding whitespace before parsing a duration in Hash.from_xml
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
Hash.from_xmlがtype="duration"の要素をパースする際に、前後の空白文字(改行・インデント)を除去してからActiveSupport::Durationに変換するように修正された PR です。これにより、「人間が整形した XML」に含まれる改行やインデントのせいでParsingErrorが起きていた問題が解消されます。
- 変更内容の詳細
何が問題だったか
以下のように、整形された XML で type="duration" のノードに改行やインデントが含まれると:
Hash.from_xml(<<~XML)
<event>
<length type="duration">
PT1H
</length>
</event>
XML実際のテキストノードの値は " PT1H\n " のように前後に空白を含む形になり、ActiveSupport::Duration::ISO8601Parser がこれを正しく解釈できず、ParsingError を送出していました。
同じような書き方でも、type="date", type="datetime", type="integer", type="boolean" などは内部で strip されている(または許容度が高い)ため問題なくパースできていましたが、duration だけが厳格だった状態です。
今回の修正内容
activesupport/lib/active_support/xml_mini.rb で、type="duration" の値をパースする前に strip をかけるように 1 行差し替えが行われました。
イメージとしては:
when "duration"
# 変更前(概念的に):
ActiveSupport::Duration.parse(value)
# 変更後(概念的に):
ActiveSupport::Duration.parse(value.to_s.strip)
end※実際のコードは若干異なる可能性がありますが、意味としては「文字列に変換して前後の空白を削る」の 1 行の修正です。
テスト
activesupport/test/xml_mini_test.rb に 1 行のテストケースが追加されています。
内容は、インデント付きの type="duration" ノードを含む XML を Hash.from_xml に渡したときに、ActiveSupport::Duration(たとえば 1.hour)として正しくパースされることを検証するものです。
これにより、今回の回避策が将来のリグレッションで壊れないようにカバーされています。
- 影響範囲・注意点
影響範囲
ActiveSupport::XmlMini経由でHash.from_xmlを用い、type="duration"のノードを扱う箇所すべてに影響があります。- インデントや改行を含む「整形された XML」や、人手で書かれた XML を扱うアプリケーションで特に恩恵があります。
- これまで
type="duration"だけ他の型と比べて扱いがシビアだった点が解消され、dateやdatetimeなどと同様にプレーンテキストに余分な空白があっても安全にパースされるようになります。
後方互換性 / 互換性リスク
- 前後の空白を
stripするだけなので、通常の利用ではほぼリスクはありません。 - ISO8601 の duration 文字列の意味的に、先頭・末尾の空白に意味を持たせるケースは現実的には想定されず、互換性問題はほぼ発生しないと考えられます。
- もし「わざと先頭/末尾に空白を入れてパースエラーを起こさせる」といった挙動に依存しているコードがあれば、その挙動は変化します(エラーにならずパースに成功するようになる)が、そのような依存は一般的にはありえないはずです。
- 前後の空白を
運用・テスト上の注意
- 既存の XML をそのまま
Hash.from_xmlに渡してduration周りでエラーになっていた場合、今回の変更を含む Rails バージョンに上げることで自然に解消されます。 - XML の仕様やフォーマッタの変更は不要です。既存の pretty-print やインデント付き XML をそのまま利用できます。
- 既存の XML をそのまま
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58015
タイトル: Strip surrounding whitespace before parsing a duration inHash.from_xml
作者: @55728 - 関連クラス:
ActiveSupport::XmlMiniActiveSupport::Duration::ISO8601Parser
- 類似仕様:
type="date",type="datetime",type="integer",type="boolean"など他の型では、既にプレーンテキストの前後空白に起因する問題は発生していませんでしたが、durationもその挙動に揃えられた形になります。
#58014 Remove stale payload-based filter example in EventReporter#subscribe rdoc
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
EventReporter#subscribe の RDoc に載っていた「payload によるフィルタ」サンプルコードが、実際の挙動と矛盾しており誤解を招くため削除され、フィルタから参照できるのはイベント名のみであることを明示するドキュメント修正です。コードの挙動には一切変更はなく、ドキュメントのみの更新です。
- 変更内容の詳細(あればサンプルコードも含めて)
変更の背景
Rails.event.subscribe の RDoc には、フィルタの例として次の2パターンが記載されていました(問題のある方だけ抜粋):
Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }しかし、実際にフィルタブロックに渡される event は「イベント名だけを持つハッシュ」であり、event[:payload] は常に nil になります。そのため、このサンプルをコピペして使うと、条件が常に false になり、イベントを一件も受け取れないという不具合を招きます(ただしコード上は例外も出ないため気づきにくい)。
モジュールレベルの「Filtered Subscriptions」の説明ではすでに:
Note that only the +:name+ key is available to filters, not the entire payload.
と明記されており、#subscribe の例だけがこれと矛盾していました。
実際の変更内容
変更ファイル: activesupport/lib/active_support/event_reporter.rb
行レベルでは以下が行われています(概要ベース):
#subscribeの RDoc から、event[:payload]を使ったフィルタ例を削除- 代わりに、「フィルタから使えるのは
:nameキーのみであり、payload 全体は参照できない」という注意書きを、クラス(モジュール)レベルの説明と同様に#subscribeのドキュメントにも追記
具体的な正しい利用イメージは、たとえば次のような形になります:
# OK: イベント名でフィルタする例
Rails.event.subscribe(->(event) { Rails.logger.info(event) }) do |event|
event[:name] == "user.created"
endevent で使えるのは event[:name] だけで、payload はここには来ない、という点を強調する形に修正されています。
- 影響範囲・注意点
- 影響範囲は「ドキュメントのみ」であり、
ActiveSupport::EventReporter/Rails.event.subscribeの挙動そのものは以前から変わっていません。 - すでに
event[:payload]をフィルタ内で参照していたコードがある場合、それはこの PR の前から期待通りには動いておらず、「常に false になってイベントが一件も流れない」状態だった可能性が高いです。- もしそのようなコードが見つかった場合:
- フィルタ部分では「イベント名のみ」を使ってフィルタする
- payload ベースの条件分岐は「サブスクライバー側(購読ブロックの中)」で行う
といった構成に変更する必要があります。
- もしそのようなコードが見つかった場合:
- テストや実装を壊す類の変更ではないため、Rails を更新したことで挙動が変わることはありませんが、「今までたまたま気づかずに誤ったサンプルを真似していた」ケースを洗い出す良いトリガーにはなります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58014
- 関連ドキュメント:
ActiveSupport::EventReporterの「Filtered Subscriptions」セクション- フィルタに渡されるハッシュは
{:name => "event_name"}のように:nameのみを保持し、payload は含まれないという仕様が明文化されています。
- フィルタに渡されるハッシュは
#58005 Make add_column(if_not_exists: true) reversible
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
add_column(..., if_not_exists: true)を含むマイグレーションをロールバックした際に、逆操作として不正なオプションを付けたremove_columnが生成されていた問題を修正し、正しくif_exists: trueが付くようにした PR です。これにより、該当マイグレーションのロールバックが安全かつ他のコマンドと一貫した挙動になります。
- 変更内容の詳細
何が問題だったか
以下のようなマイグレーションを書いた場合:
add_column :users, :age, :integer, if_not_exists: truerails db:rollback などで逆マイグレーションを生成・実行すると、これまでは CommandRecorder によって次のように解釈されていました:
# BEFORE (誤った逆操作)
remove_column :users, :age, :integer, if_not_exists: trueしかし remove_column は if_not_exists オプションを受け付けません。そのため:
- 逆マイグレーションとして不正なメソッド呼び出しが生成される
- 将来的にはエラーになったり、意図しない挙動を起こす可能性がある
という問題がありました。
今回の修正内容
ActiveRecord::Migration::CommandRecorder において、add_column の逆操作 (invert_add_column) を組み立てる際のオプション変換ロジックが追加されました。
挙動は次のように変わります:
# 正方向
add_column :users, :age, :integer, if_not_exists: true
# 逆方向 (今回の PR 以降)
remove_column :users, :age, :integer, if_exists: truePR の説明にもある通り:
# inverse of add_column(:table, :column, :type, if_not_exists: true)
# before: remove_column(:table, :column, :type, if_not_exists: true)
# after: remove_column(:table, :column, :type, if_exists: true)つまり、if_not_exists を if_exists に変換してから remove_column に渡すようになりました。
他のコマンドとの一貫性
add_index, add_foreign_key, add_check_constraint ではすでに同様の変換が実装されており:
add_*(..., if_not_exists: true)
の逆操作としてremove_* ( ..., if_exists: true)
が生成されています。
今回の PR で add_column もその設計方針に揃えられました。
テスト
activerecord/test/cases/migration/command_recorder_test.rb にテストが追加され、add_column(..., if_not_exists: true) を記録して逆変換した際に if_exists: true が付いた remove_column が生成されることが検証されています。
- 影響範囲・注意点
- 対象:
add_column :table, :column, :type, if_not_exists: trueを使用しているマイグレーション- それらを
rails db:rollbackやrevertで巻き戻すケース
- 影響:
- 以前は「見かけ上」
remove_column(..., if_not_exists: true)が生成されていましたが、これはそもそも正当な API ではありませんでした。 - 今後は正しく
remove_column(..., if_exists: true)になるため、「すでにカラムが削除されている場合」でもエラーにならない、より安全なロールバックになります。
- 以前は「見かけ上」
- 後方互換性:
- 公開されていない・公式にサポートされていない挙動(
remove_columnにif_not_existsを渡す)を修正する変更であり、通常のアプリケーションコードには実害はほぼありません。 - もしテストやアプリケーション側で
CommandRecorderの生の出力を文字列ベースで厳密に検証している場合は、if_not_exists→if_existsへの変更によりテストが変わる可能性があります。
- 公開されていない・公式にサポートされていない挙動(
- マイグレーションの書き方としての注意:
- 今後も、冪等にしたいマイグレーションでは:
- 追加系:
add_column ..., if_not_exists: true - 削除系:
remove_column ..., if_exists: trueを明示的に書くのが推奨されますが、今回の修正により、逆操作生成の際にもこのペアが自動で維持されるようになります。
- 追加系:
- 今後も、冪等にしたいマイグレーションでは:
- 参考情報 (あれば)
- 当該 PR:
rails/rails#58005 “Make add_column(if_not_exists: true) reversible” - 関連する他の reversible な API 例:
add_index(..., if_not_exists: true)⇔remove_index(..., if_exists: true)add_foreign_key(..., if_not_exists: true)⇔remove_foreign_key(..., if_exists: true)add_check_constraint(..., if_not_exists: true)⇔remove_check_constraint(..., if_exists: true)
これらと同様のパターンが add_column にも適用された、という位置づけの変更です。
#58004 Return no rows without a query for none.ids
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
Relation#noneに対してids/async_idsを呼び出したとき、これまでは実際にSELECT ... WHERE (1=0)のクエリが発行されていたのを、pluckと同様にクエリを投げずに常に空配列(またはそれを返す Promise)を返すように修正した PR です。noneが「DB に触れない null object」として一貫した動作をするようになります。
- 変更内容の詳細
背景
ActiveRecord::Relation#none は「必ず結果が 0 件で、かつ DB にクエリを投げない」ことを意図した null relation です。
- 既に
pluckなど一部メソッドは@noneフラグを見て、DB に触れずに結果を返していました。 - 一方で
idsはnoneを特別扱いしておらず、内部の条件が"1=0"という生の SQL だったため、矛盾判定(例:where(id: []))に引っかからず、実クエリが発行されていました。
その結果、以前は:
Post.none.ids # => 実際には SELECT "posts"."id" FROM "posts" WHERE (1=0)
Post.none.pluck(:id) # => [] (クエリ無し)という不整合な挙動になっていました。
何をしたか
calculations.rb 内の ids / async_ids に、pluck と同様の @none チェックを追加しています。
擬似コードレベルでは以下のようなガードが入っています(実装からの意訳):
def ids
if @none
[]
else
# 従来どおりの ids のロジック(pluck(:id) 相当)
end
end
def async_ids
if @none
# すでに完了した Promise(Future)を返し、その値は [] になる
else
# 従来どおり非同期で ids を取得
end
end結果として:
Post.none.ids # => [] (クエリ発行なし)
Post.none.pluck(:id) # => [] (クエリ発行なし)
Post.none.async_ids # => 完了済み Promise, value は [] (クエリ発行なし)となり、none の定義(「DB に触れず常に空」)とテストの期待どおりの挙動になります。
テスト
activerecord/test/cases/null_relation_test.rb にテストが追加され、null_relation に対する ids の挙動(クエリを投げずに空配列を返すこと)がカバーされています。既存の pluck, delete_all, update_all, exists?, count などのテストと並ぶ形で「null relation は DB に触れない」という性質の一貫性を確認しています。
- 影響範囲・注意点
対象メソッド
ActiveRecord::Relation#idsActiveRecord::Relation#async_ids
挙動の変化
Model.none.idsが、これまでの「SELECT ... WHERE (1=0)を実行して空配列を返す」から、「クエリを打たずに即座に空配列を返す」に変わります。async_idsも同様に、「クエリを打たない完了済み Promise(値は[])」を返します。
後方互換性
- 戻り値(
[])は変わらないため、普通にアプリ側でnone.idsを使っている限りは機能的な互換性は保たれています。 - 影響し得るのは、以下のようなケースです:
- 「
none.idsでクエリが走ること」を前提にしてメトリクス・ログ収集・モンキーパッチなどをしていた場合(ほぼ無いはずですが)。 - 「クエリが走るかどうか」を検査するテストを書いていた場合(
assert_sqlなどでWHERE (1=0)を期待しているようなテストは失敗する可能性があります)。
- 「
- 戻り値(
パフォーマンス
noneを通るパスについては、不必要なクエリが完全に削除されるため、ごくわずかですがパフォーマンス改善になります。- 特に大量に
noneが生成される複雑なスコープチェーンでidsを呼んでいるようなケースでは、微妙なオーバーヘッド削減になります。
一貫性
noneが関与する以下のメソッドの挙動が揃います:pluck,ids,async_ids,delete_all,update_all,exists?,count, …
- 「
noneからは絶対にクエリを出さない」という前提でコードや最適化・テストが書けるようになります。
- 参考情報 (あれば)
- 修正対象:
activerecord/lib/active_record/relation/calculations.rb - テスト:
activerecord/test/cases/null_relation_test.rb - 関連知識:
Relation#noneは「常に空」の relation を返すための標準 API で、default_scope, 条件付きスコープ, 認可ロジックなどで「このユーザーには 1 件も見せない」ことを表現するときに頻用されます。idsは内部的にpluck(primary_key)に近い実装になっており、今回の修正でnoneに対する挙動がpluckと明示的に揃えられました。
#58007 Instrument the normalized key in MemoryStore#increment/#decrement
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::MemoryStoreのincrement/decrementが発行する計測イベント(cache_increment/cache_decrement)について、これまで「生のキー」を送っていたものを、他のストア実装と同様に「正規化済み(名前空間付き)のキー」を送るように揃えた変更です。これにより、メモリストア利用時の計測イベントのキー情報が一貫したものになります。
- 変更内容の詳細
何が問題だったか
MemoryStore#increment / #decrement が ActiveSupport のインストゥルメンテーション(ActiveSupport::Notifications)で発行するイベント:
cache_increment.active_supportcache_decrement.active_support
のペイロード :key に、キャッシュストア内部で実際に使われている「正規化後キー」ではなく、呼び出し側が渡した「生のキー」をそのまま入れていました。
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")
cache.increment("counter")
# 以前:
# payload[:key] #=> "counter" (namespace が反映されていない)
#
# 期待される & 今回の修正後:
# payload[:key] #=> "myapp:counter"他のストア (FileStore, MemCacheStore, RedisCacheStore) や同じ MemoryStore の read / write イベントでは、すでに「正規化済みキー」を使っていたため、インクリメント/デクリメントだけ挙動がずれていました。
具体的な修正内容
変更ファイル:
activesupport/lib/active_support/cache/memory_store.rbactivesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
ポイント:
MemoryStore#increment/#decrementのインストゥルメンテーション時に、payload[:key]として 正規化後のキー を渡すように修正- 正規化処理は
normalize_key/normalize_key相当の内部ヘルパを通す形(既存の read/write と同じパス)に変更されたと考えられます。 - 実質的には
namespaceなどが反映された「実際にストア内で利用しているキー」をイベントのキーとして報告します。
- 正規化処理は
テスト (
cache_instrumentation_behavior) の修正- インクリメント/デクリメントの計測イベントにおける
payload[:key]が名前空間付きであることを確認するアサーションが追加・修正。 - これにより、将来的に再度「生のキー」を返すような退行があった場合に検出できるようになっています。
- インクリメント/デクリメントの計測イベントにおける
サンプル(概念的なイメージ):
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")
ActiveSupport::Notifications.subscribe("cache_increment.active_support") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts event.payload[:key]
end
cache.increment("counter")
# 修正前: "counter"
# 修正後: "myapp:counter"- 影響範囲・注意点
- 影響対象
ActiveSupport::Cache::MemoryStoreを使っていてcache_increment.active_support/cache_decrement.active_supportイベントを購読しpayload[:key]を直接参照しているコード
に影響があります。
挙動の変化
namespaceオプションを使っていない場合:
正規化後キーも元キーと同じになるため、実質的な変化はほぼありません。namespaceを使っている場合:
これまでpayload[:key]が"counter"だったものが、"myapp:counter"のように名前空間付きになります。- それ以外にも、
normalize_keyで行っている正規化(#to_paramや encoding 調整など)があれば、その結果が反映されます。
後方互換性の観点
- 監視基盤やメトリクス集計で、「このイベントの
keyは必ず非 namespaced な生キー」という前提でロジックを書いていた場合、集計キーが変わる可能性があります。 - 他ストア(File/MemCache/Redis)と挙動を合わせた変更のため、「複数ストアで共通の購読コード」を書いているプロジェクトでは、むしろ今回の変更で挙動が一貫し、扱いやすくなります。
- 監視基盤やメトリクス集計で、「このイベントの
対応の指針
- もし「アプリケーションレベルの論理名のキー」と「実際のストアキー(namespace 付き)」を区別してメトリクスを取りたい場合は、今後は以下のような設計を検討するとよいです:
- イベントから受け取った
payload[:key]を「実キー」とみなし、 - 論理キーは別途 payload に入す(独自インストゥルメンテーション)か、キー生成側でロギングする。
- イベントから受け取った
- もし「アプリケーションレベルの論理名のキー」と「実際のストアキー(namespace 付き)」を区別してメトリクスを取りたい場合は、今後は以下のような設計を検討するとよいです:
- 参考情報 (あれば)
- 該当 PR:
Instrument the normalized key in MemoryStore#increment/#decrement(#58007) - 関連する他ストアの挙動:
ActiveSupport::Cache::FileStoreActiveSupport::Cache::MemCacheStoreActiveSupport::Cache::RedisCacheStore
これらはいずれもインストゥルメンテーションで「正規化済みキー」を報告しており、今回の変更でMemoryStoreもそれに揃った形です。
- 関連テスト:
activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
ここを読むと、ActiveSupport が「計測イベントにどういう情報を載せることを期待しているか」のリファレンスとしても利用できます。
#58008 Clear a stale enqueue_error when perform_all_later re-enqueues a job
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
perform_all_laterがenqueue_all未実装アダプタ向けに「1件ずつ enqueue」するフォールバック経路を使う際、過去の失敗時にセットされたenqueue_errorがクリアされず、状態が矛盾する問題を修正したPRです。これにより、再enqueueに成功したジョブはsuccessfully_enqueued? == trueかつenqueue_error == nilという一貫した状態になります。
- 変更内容の詳細
背景
- Active Job には複数ジョブをまとめてキューに積む
perform_all_laterがあり、アダプタがenqueue_allを実装していない場合は、内部的に各ジョブに対して個別にenqueueを呼ぶフォールバックが行われます。 - 単一ジョブの
enqueue経路では、enqueue を試行する前にenqueue_errorをクリアする仕様になっており、「前回の enqueue 失敗情報を引きずらない」ようになっています。 - しかし、
perform_all_laterのフォールバック経路ではsuccessfully_enqueuedフラグはリセットしていた一方で、enqueue_errorをクリアしていませんでした。
問題となっていた挙動
- あるジョブが何らかの理由で enqueue に失敗し、
enqueue_errorがセットされた状態になったとします。 - そのジョブを後で
perform_all_laterのフォールバック経路(=enqueue_all非対応アダプタ)経由で再enqueueし、今度は成功した場合でも、古いenqueue_errorが残ってしまっていました。 - この結果、同じジョブインスタンスに対して:
job.successfully_enqueued? # => true
job.enqueue_error # => #<SomeError ...> (古いエラーが残っている)という矛盾した状態が起きていました。
このPRでの修正内容
- フォールバック経路(各ジョブを個別に enqueue し直すロジック)の中で、再enqueueを試みる前に
enqueue_errorもクリアするように変更。 - 単一ジョブの
enqueue経路と同様に、「enqueue を試行する際には過去のエラー情報をリセットする」挙動に揃えています。
擬似的には以下のようなイメージです(実際のコードは簡略化):
jobs.each do |job|
job.successfully_enqueued = false
job.enqueue_error = nil # ← この行が追加されたイメージ
queue_adapter.enqueue(job)
endテストの追加
activejob/test/cases/queuing_test.rb にテストが追加され、以下のようなケースが検証されています:
- 以前の enqueue 失敗で
enqueue_errorがセットされているジョブがある。 perform_all_later実行時、アダプタがenqueue_allを実装していないため 1件ずつ enqueue される。- enqueue が成功したジョブは:
successfully_enqueued?がtrueenqueue_errorがnilであることを確認。
- 影響範囲・注意点
- 対象となるのは「
enqueue_allを実装していない Active Job アダプタ」を使用し、かつ「同じジョブインスタンスを再利用してperform_all_laterで再enqueue する」ようなケースです。 - これまで「ジョブは正常に enqueue されているのに
enqueue_errorが残っている」状態に依存したコード(例えば、enqueue_errorの有無だけで前回の失敗を判定していたりする処理)があれば、挙動が変わります。ただし、本来は不整合な状態だったため、通常は修正として望ましい挙動です。 enqueue_allを実装しているアダプタにはこの変更は直接影響しません(そちらは元々enqueue_allの実装側の責務になります)。
- 参考情報 (あれば)
- 目的は「単一ジョブ enqueue とバッチ enqueue(
perform_all_laterのフォールバック)の状態管理を対称に保つ」ことです。 - Active Job でカスタムアダプタや
enqueue_allの実装を行う場合も、同様に「成功したジョブにはenqueue_errorを残さない」という一貫性を持たせるのが望ましい設計になります。
#58009 Account for queue_name_prefix in assert_enqueued_email_with
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
ActiveJob::Base.queue_name_prefixを設定している場合でも、assert_enqueued_email_withが正しくジョブのキュー名を判定できるように修正した PR です。これにより、メール自体は正しくdeliver_laterされているのにテストだけが失敗する問題が解消されます。
- 変更内容の詳細
問題の背景
config.active_job.queue_name_prefix を設定すると、Active Job ではキュー名が以下のように変更されます:
config.active_job.queue_name_prefix = "prefix"
# 通常 "mailers" になるところが "prefix_mailers" になる
TestMailer.test.deliver_laterしかし、これまでの ActionMailer::TestHelper#assert_enqueued_email_with は、
「メール系ジョブは mailers キューに入るはず」という前提で期待キュー名を決めており、queue_name_prefix などの設定を考慮していませんでした。
そのため、以下のようなコードはジョブ自体は正しく prefix_mailers に enqueue されているにもかかわらず、テストが「mailers に入っていない」と誤判定して失敗していました。
config.active_job.queue_name_prefix = "prefix"
assert_enqueued_email_with(TestMailer, :test) do
TestMailer.test.deliver_later
end
# before: 失敗 (期待 "mailers" / 実際 "prefix_mailers")
# after: 成功具体的な修正点
変更ファイルは 2 つだけです。
actionmailer/lib/action_mailer/test_helper.rbassert_enqueued_email_with内で「期待するキュー名」を決定する処理を、実際の配送ジョブ(ActionMailer::MailDeliveryJob等)が enqueue 時にキュー名を決定するロジックと同じ方法で導出するように修正。- つまり、
queue_name_prefixやqueue_name_suffixが設定されていれば、それを含んだ完全なキュー名(例:"prefix_mailers","prefix_mailers_suffix")を期待値として使うようになりました。 - コード上は 1 行の差し替えで、直接
"mailers"を見るのではなく、ActiveJob が使うキュー名解決ロジックを経由する形になったと考えられます。
actionmailer/test/test_helper_test.rb- この挙動を保証するテストが追加されています (+15 行)。
- 内容としては、
config.active_job.queue_name_prefixを設定した状態でTestMailerのdeliver_laterを呼び、assert_enqueued_email_withがパスすることを検証するテストです。 - これにより、今後
queue_name_prefixの仕様変更やassert_enqueued_email_withの改修があっても、プレフィックス付きキュー名への対応が壊れないようにしています。
- 影響範囲・注意点
影響を受けるのは「
ActionMailer::TestHelper#assert_enqueued_email_withを使っており、かつconfig.active_job.queue_name_prefix(または suffix)を使っているプロジェクト」です。- これまで、そのようなプロジェクトでは「メールは正常にキューに積まれているのにテストが落ちる」という症状が出ていたはずです。
- この PR 適用後は、そのようなテストは通るようになります。
メール配送自体の挙動 (
deliver_later時の queue や実行タイミング) には変更がなく、「テスト用ヘルパーの期待値ロジックのみ」が修正対象です。そのため、本番環境の挙動変更リスクはほぼありません。もし既にワークアラウンドとして
- 自前で
queue: "prefix_mailers"を指定して検査していたり、 assert_enqueued_email_withを使わずに低レベルなassert_enqueued_jobsなどで検査していた といったケースがある場合、この修正によりワークアラウンドが不要になる可能性があります。
- 自前で
queue_name_suffixも「prefix と同様の仕組みで解決」されるため、この PR によって suffix を用いた構成でも期待どおりに動作するようになります(prefix/suffix を総合した「実際のキュー名」にテストが揃う)。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58009
- 関連する Rails ガイド:
- Active Job Basics: Queue configuration
config.active_job.queue_adapterconfig.active_job.queue_name_prefixconfig.active_job.queue_name_delimiterconfig.active_job.queue_name_suffix
- Active Job Basics: Queue configuration
- 関連 API:
ActionMailer::TestHelper#assert_enqueued_email_withActiveJob::Base.queue_name_prefix/queue_name_suffixActionMailer::MailDeliveryJob(メール配送ジョブの実装)
#58010 Support endless and beginless ranges in number_field/range_field
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
number_field/range_fieldヘルパで、in:/within:オプションに無限範囲 (18..など) や片側無限範囲 (..10など) を渡したときにRangeErrorになっていた問題を修正し、適切にmin/max属性へ変換できるようにした PR です。Ruby の end/beginless Range を正式にサポートしつつ、従来の閉区間・排他的区間の挙動は維持しています。
- 変更内容の詳細 (サンプルコード含む)
変更の主眼
- 対象ヘルパ:
ActionView::Helpers::FormOptionsHelper#number_fieldActionView::Helpers::FormOptionsHelper#range_field
- オプション:
in:/within:に渡されるRangeの扱いを修正
以前は、以下のような範囲を渡すと RangeError が発生していました。
number_field("order", "quantity", in: 18..) # endless range
number_field("order", "quantity", in: ..10) # beginless rangeこの PR により、以下のように HTML 属性へマッピングされるようになります。
Endless range (18..)
number_field("order", "quantity", in: 18..)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" min="18" />
# (max 属性は付与されない)Beginless range (..10)
number_field("order", "quantity", in: ..10)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" max="10" />
# (min 属性は付与されない)既存の bounded range の挙動はそのまま
通常の閉区間(両端あり、
..):rubynumber_field("order", "quantity", in: 1..10) # <input ... type="number" min="1" max="10" />排他的範囲 (
...) も従来通り:rubynumber_field("order", "quantity", in: 1...10) # => max は「上限 - 1」としてレンダリングされる # <input ... type="number" min="1" max="9" />
PR 説明によると、この既存の「exclusive range は max を 1 小さくする」ロジックもそのまま維持されています。
実装変更の概要
actionview/lib/action_view/helpers/tags/number_field.rbのRangeからmin/maxを決定する部分を、endless / beginless のRangeを許容するように 1 行修正。actionview/test/template/form_helper_test.rbにテストケースを追加 (+10 行):- endless range を渡したときに
minのみ設定されること - beginless range を渡したときに
maxのみ設定されること - 例外が発生しないこと
- endless range を渡したときに
テスト追加により、今後のリグレッションも検知できるようになっています。
- 影響範囲・注意点
影響範囲
number_field,range_fieldをin:/within:オプション付きで利用しているフォームビューが対象。- 特に Ruby 2.6+ などで導入された end/beginless range 構文を既に利用しているコードで、以前は例外が発生していたケースが正常に動作するようになります。
- フロントエンド的には、
min/max属性の有無だけが変わるため、HTML5 バリデーションロジックや、JavaScript 側でmin/maxの有無に依存している場合に影響し得ます。
仕様面の注意点
in:/within:に Range を渡した場合の挙動は以下に整理されます:- 両端あり (
a..b) →min=a,max=b - 排他的 (
a...b) →min=a,max=b-1 - endless (
a..) →min=a,maxなし - beginless (
..b) →max=b,minなし
- 両端あり (
- Ruby の「通常の Range オブジェクトを受け付ける」という Rails のドキュメント仕様と整合しており、
RangeErrorを投げてしまうこれまでの挙動がよりどころのない状態だったため、それを是正する変更といえます。 - これにより、サーバーサイドバリデーション (
validates :field, inclusion: a..) とフォーム側の表示 (number_field ..., in: a..) を、end/beginless range を含めて一貫して書きやすくなります。
後方互換性
- これまで
RangeErrorをトリガーする前提で rescue するようなコードを書いていた場合、その挙動は変わる可能性がありますが、通常のアプリケーションではそういう利用はまず想定されないため、実質的には後方互換性を損なわない変更と考えられます。 - bounded range (特に
1...10のような排他的 range) の既存挙動には一切変更がない点がポイントです。
- これまで
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58010
- 関連する Rails ドキュメント:
- Action View Form Helpers –
number_field,range_fieldの:in/:withinオプション
- Action View Form Helpers –
- Ruby の endless / beginless range 仕様:
a../..bは Ruby 2.6 以降で導入された Range の構文であり、通常のRangeオブジェクト ((18..) === 20 #=> true) として扱われます。
#58006 Return the current value for an unchanged store accessor's _was
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
store アクセッサに対する<key>_wasの挙動が、変更されていない場合にもnilを返してしまう不整合を修正し、ActiveModel::Dirtyの契約どおり「未変更なら現在値を返す」ように揃えた PR です。これにより、store カラムを使う場合も通常の属性と同様の Dirty API の挙動が得られます。
- 変更内容の詳細
挙動の変更点
修正前(未変更時):
# 例: jsonb カラム `settings` に対して store_accessor で color を定義
# record.settings = { "color" => "black" }
record.color # => "black"
record.color_was # => nil # ← 本来期待される挙動と異なる修正後(未変更時):
record.color # => "black"
record.color_was # => "black" # ← 通常の attribute と同じ挙動値を変更した後の挙動は従来どおりで、過去の値を返します:
record.color # => "black"
record.color = "white"
record.color_was # => "black" # 変更前の値
record.color_change # => ["black", "white"] # こちらは既に修正済みだった挙動と整合
record.color_changed? # => trueどのように修正されたか(概念レベル)
activerecord/lib/active_record/store.rbの store アクセッサ定義部分で、<key>_wasが未変更時にnilを返さないように 1 行分のロジックが修正されています。ActiveModel::Dirty#attribute_wasと同様、「その属性が変更されていなければ、_wasは現在値を返す」ように挙動を合わせています。- これまでに
_change(<key>_change)については未変更時の挙動が既に修正済みだったため、今回の変更で_wasも含めて Dirty 系メソッド間で一貫した挙動になります。
テスト
activerecord/test/cases/store_test.rb に 9 行追加されています。
想定されるテスト内容(要約):
- store アクセッサを持つモデルを用意し、レコードを保存した直後(未変更状態)に
<key>_wasを呼び出すと現在値を返すことを確認。 - 値を更新した後は、変更前の値を返すことを確認。
_changeや Dirty 判定との整合性を担保。
- 影響範囲・注意点
影響範囲
対象となるのは:
store/store_accessorを使っているモデル- かつ、
<key>_wasを明示的に参照しているコード
に限定されます。
これまで:
- 「store アクセッサの
<key>_wasは、未変更時にはnilを返す」と仮定したコード(ワークアラウンド)を書いていた場合、その前提が崩れます。
これから:
- 通常の ActiveRecord 属性と同様に、
- 未変更時:
<key>_wasは現在値 - 変更後:
<key>_wasは変更前の値 となります。
- 未変更時:
移行・互換性上の注意点
もし次のようなコードを書いていた場合は挙動が変わります:
ruby# 例: nil かどうかで「未設定かどうか」を判定しているケース if record.color_was.nil? # 以前は「未変更か、元々 nil」かを区別できないが、 # 実際には「未変更 = nil」「変更済み = 過去値(non-nil)」のように使っていた可能性がある endこのような用途には Dirty API を直接使う方が正確です:
rubyif record.color_changed? # 変更された場合の処理 before, after = record.color_change end「未変更かどうか」を知りたいなら
_wasではなく<key>_changed?<key>_changesaved_change_to_<key>?/saved_change_to_<key>(永続化後の変更検知) を利用するのが筋となります。
nil自体を「未設定」の意味で使っている store キーの場合、- 未変更時に
<key>_wasがnilを返す挙動は変わりません(値がnilだから)。 - ただし、以前は「未変更だが
nil」と「変更済みだが元値がnil」を_was.nil?では区別できなかった点は変わらず、Dirty API(_changed?等)で区別する必要があります。
- 未変更時に
- 参考情報 (あれば)
- 関連 API:
ActiveModel::Dirty#attribute_wasActiveModel::Dirty#attribute_changeActiveModel::Dirty#attribute_changed?
- store / store_accessor の概要:
store :settings, accessors: %i[color size]record.color/record.color=がsettings["color"]を透過的に扱う
- この PR で
_changeと_wasの挙動が揃ったため、store アクセッサも「普通のカラム属性」と同じ感覚で Dirty 情報を扱えるようになっています。
#58011 Honor an explicit :selected option in weekday_select
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
weekday_selectヘルパにselected:オプションを明示的に渡した場合でも、その値が正しく優先されるように挙動が修正されました。これにより、他のselect系ヘルパ (select,collection_select,grouped_collection_select,date_select) と一貫した動きになります。
- 変更内容の詳細
これまでの挙動
weekday_select は、フォームオブジェクト(例: @digest)側に既に曜日の値が入っていると、selected: オプションを無視してオブジェクトの値を選択状態にしていました。
# モデル側の値
@digest.send_day #=> "Monday"
# ビュー側
f.weekday_select(:send_day, selected: "Friday")上記のように書いても、実際に選択されるのは "Monday" でした。
修正後の挙動
同じコードを実行した場合、selected: "Friday" が優先され、 "Friday" が選択状態になります。
# 修正後
f.weekday_select(:send_day, selected: "Friday")
# => セレクトボックス上では "Friday" が選択される実装面の変更ポイント
actionview/lib/action_view/helpers/tags/weekday_select.rbのロジックが 1 行修正され、
「オブジェクトの属性値」よりも「オプションで明示された:selected」を優先するようになりました。actionview/test/template/form_options_helper_test.rbにテストが追加され、- オブジェクトに曜日がセットされている
- かつ
selected:オプションが指定されている
というケースで、明示的selectedが優先されることが保証されています。
他の select 系ヘルパと同様に、「明示的に指定された selected があればそれを使う」という Rails の一貫したポリシーに合わせた変更です。
- 影響範囲・注意点
影響範囲
weekday_selectを使用しており、かつモデルにデフォルトの曜日が格納されているフォームでselected:オプションも指定している場合、表示される初期選択が変わる可能性があります。- これまで「オブジェクトの値が優先される」前提で
selected:を“ダミー”的に書いていたコードは、明示指定の方が効くようになるため挙動が変わります。
互換性の観点
- 他の
select系ヘルパと同じルールになるため、API 全体としては一貫性が向上します。 - ただし、
weekday_selectだけを個別仕様として利用していた場合は挙動変更となるため、フォームの初期選択状態の変化に注意してください。
- 他の
テスト
- 対応するテストが追加されているため、
selected:の優先挙動は今後も守られることが期待できます。
- 対応するテストが追加されているため、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58011
- 類似の挙動を持つヘルパ:
ActionView::Helpers::FormOptionsHelper#selectActionView::Helpers::FormOptionsHelper#collection_selectActionView::Helpers::FormOptionsHelper#grouped_collection_selectActionView::Helpers::DateHelper#date_select
これらと同じく、「オブジェクトの値 < 明示的な selected オプション」という優先順位になった、と理解しておくと良いです。
#58003 Fix belongs_to change tracking for composite foreign keys
マージ日: 2026/7/5 | 作成者: @anxkhn
- 概要 (1-2文で)
belongs_toに複合外部キー(Composite Foreign Key)を使っている場合に、association_changed?/association_previously_changed?が常にfalseを返してしまうバグを修正したPRです。単一カラムの外部キーには影響せず、複合キーのときだけ正しく変更検知できるようにしています。
- 変更内容の詳細
問題点
belongs_to で複合外部キーを使うと、例えば以下のような関連づけになります:
class Cpk::Order < ApplicationRecord
self.primary_keys = :shop_id, :id
end
class Cpk::Book < ApplicationRecord
belongs_to :order,
class_name: "Cpk::Order",
foreign_key: [:shop_id, :order_id]
endこのとき Rails 内部では reflection.foreign_key が:
["shop_id", "order_id"]という配列になっています。
ところが、BelongsToAssociation#target_changed? および #target_previously_changed? は、この reflection.foreign_key をそのまま「1つの名前」とみなして、スカラ用の dirty メソッドに渡していました:
attribute_changed?(reflection.foreign_key) # 実際のイメージ
attribute_previously_changed?(reflection.foreign_key) # 実際のイメージスカラの dirty メソッド (attribute_changed?) は引数に対して to_s するので、配列は:
["shop_id", "order_id"].to_s
# => '["shop_id", "order_id"]'となってしまい、「そんな属性名は存在しない」ため、常に false になっていました。その結果、belongs_to :order に対して自動生成される:
order_changed?
order_previously_changed?などの公開APIが、複合キーのときだけ常に「変化なし」と報告してしまう状態でした。
単一カラムの外部キー ("shop_id" など) では to_s しても "shop_id" のままなので、この問題が表に出ず、これまで見逃されていました。
修正内容
BelongsToAssociation#target_changed? と #target_previously_changed? を、「外部キーが配列のときは各カラムを個別にチェックする」ように変更しています。
擬似コードにすると、以下のようなイメージです:
def target_changed?
Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) }
end
def target_previously_changed?
Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
endポイント:
Array(reflection.foreign_key)とすることで、- 複合キー:
["shop_id", "order_id"]→["shop_id", "order_id"] - 単一キー:
"shop_id"→["shop_id"]
のように正規化されます。
- 複合キー:
.any?で「どれか1つでも変わっていれば関連変更あり」と判断します。- これは同じファイル内で既に複合キー対応している他の処理 (
replace_keys,foreign_key_present?) と同じパターンで、実装を揃えた形になっています。
ポリモーフィック belongs_to への影響
BelongsToPolymorphicAssociation は super で上記ロジックを呼んだ上で、foreign_type もOR条件でチェックする実装になっています。
- 複合キーの belongs_to でポリモーフィックなケースは、
superの修正をそのまま継承。 - ポリモーフィック外部キー自体はスカラなので、今回の修正で挙動が変わることはありません。
テスト
activerecord/test/cases/associations/belongs_to_associations_test.rb に回帰テストが追加されています。
テストのシナリオ(概念的な流れ):
- 複合主キーを持つ
Cpk::Orderを2つ作成(同じshop_idでorder_idだけ違う)。 Cpk::Bookを、最初のorderにbelongs_to :orderで紐づけて保存。- その後、
book.order = 別の orderに付け替えて、shop_idは同じだがorder_idを変更。 - ここで:
book.order_changed?がtrueになることを確認。
book.save後に:book.order_previously_changed?がtrueになることを確認。
このテストは main ブランチでは両方 false になって失敗し、この修正を適用するとパスします。
- 影響範囲・注意点
影響範囲
- 対象:
belongs_toに複合外部キーを指定している関連。 - 修正されるAPI:
association_changed?(例:order_changed?)association_previously_changed?(例:order_previously_changed?)
- 単一カラム外部キーの
belongs_toの挙動は変わりません。 - ポリモーフィック
belongs_toの挙動も、実質的には従来通りです。
- 対象:
意図的に変更していない箇所
saved_change_to_target?は今回は変更していません。- これはカウンタキャッシュや
touchコールバック専用の内部APIで、ここだけ先に直すと:after_updateで新しい親のカウンタだけインクリメントされる- 一方で
decrement_counters_before_last_saveは古い親側のデクリメントをスキップしたまま
という不整合が起き、カウンタキャッシュが膨らむ危険があります。
- 複合キーに対するカウンタキャッシュ周りの扱いは #57608 で別途対応中であり、本PRでは「公開APIの
association_changed?/association_previously_changed?のバグ修正」にスコープを限定しています。
- これはカウンタキャッシュや
互換性面での注意
- これまで「複合外部キーの
belongs_toは常に*_changed?がfalseになる」ことを前提にワークアラウンドを書いていたコードがあれば、今回の修正で挙動が変わる可能性があります。- 例:
if !order_changed?を「常にここに入る」前提ロジックとして悪用(?)していた場合など。
- 例:
- 本来の意図した動作に修正されるものであり、一般的にはバグフィックスとして望ましい変更です。
- これまで「複合外部キーの
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/58003
- 関連する既知課題(複合キーとカウンタキャッシュまわり): https://github.com/rails/rails/pull/57608
- 実際に影響を確認したい場合:
- 複合外部キーの
belongs_toを持つモデルでxxx_changed?/xxx_previously_changed?を呼び、外部キーの一部だけ変えたときにtrueになるかをテストすると、挙動差を確認できます。
- 複合外部キーの
#57991 Stop HTML-escaping JSON written by Coders::JSON
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Coders::JSONが JSON を DB に書き込む際、意図されていた「HTML エスケープ無し (escape: false)」設定が実際には効いておらず、HTML エスケープされてしまっていた不具合を修正する PR です。これにより、JSON 型カラムやActiveRecord::Type::Jsonとの挙動が統一されます。
- 変更内容の詳細
問題点
ActiveRecord::Coders::JSON にはもともと、次のような方針がありました。
- JSON を DB カラムに書き込むとき、HTML エスケープ(
<→\u003cなど)をスキップするために、デフォルトオプションとしてescape: falseをマージする。
しかし実装上のバグにより:
- エンコーダは「マージ済みの
@options」ではなく、「呼び出し時に渡された生のoptions引数」で初期化されていた。 - その結果、
escape: falseが実際のエンコード処理に反映されておらず、HTML エスケープが有効のままだった。
具体例:
coder = ActiveRecord::Coders::JSON.new
coder.dump({ "k" => "<>&" })
# 修正前の実際の挙動:
# => "{\"k\":\"\\u003c\\u003e\\u0026\"}"
# 期待される挙動(ネイティブ JSON カラムと同じ):
# => "{\"k\":\"<>&\"}"PR の説明で明示されている通り、修正前は:
{"k":"\u003c\u003e\u0026"}
が返されており、これは:
- ネイティブ JSON カラム
ActiveRecord::Type::Json
によるシリアライズ結果と食い違っていました。
修正内容
ActiveRecord::Coders::JSON の内部で:
- マージ後のオプション(
escape: falseを含む)を保持している@options - 元の引数として渡された
options
のうち、エンコーダの初期化に @options を使うように 1 行だけ修正されています。
擬似コードで表すと、だいたい以下の違いです(実際のコードは若干異なりますが、意味としてはこういう修正):
# 修正前 (イメージ)
def initialize(options = {})
@options = DEFAULT_OPTIONS.merge(options) # escape: false をマージしている
@encoder = SomeJSONEncoder.new(options) # ← 生 options を使っていた
end
# 修正後 (イメージ)
def initialize(options = {})
@options = DEFAULT_OPTIONS.merge(options)
@encoder = SomeJSONEncoder.new(@options) # ← マージ後オプションを使う
endこれにより、ActiveRecord::Coders::JSON を使った場合も、デフォルトで escape: false がきちんと有効になります。
テストの追加
activerecord/test/cases/coders/json_test.rb に 5 行のテストが追加され、以下を確認しています:
Coders::JSON.new.dump({ "k" => "<>&" })の結果が{"k":"<>&"}となること- つまり、HTML エスケープされないこと
- 影響範囲・注意点
影響範囲
- 対象:
ActiveRecord::Coders::JSONを使って、DB の文字列カラムなどに JSON を保存しているケース。- 例:
serialize :settings, ActiveRecord::Coders::JSONのような実装
- 例:
- 変更内容: デフォルトで HTML エスケープされていた JSON 出力が、今後はエスケープされなくなります。
互換性の観点
- ネイティブ JSON カラム(
json/jsonb)やActiveRecord::Type::Jsonの挙動と揃うため、「Rails 内での一貫性」という意味では正しい方向です。 - 既存データ:
- 既に DB に保存済みの
"k":"\u003c\u003e\u0026"などの値はそのまま残り、新規保存分のみ"k":"<>&"風になります。 - 読み込み時はどちらも JSON として問題なく扱えるため、デシリアライズ時の挙動に違いはほぼありません。
- 既に DB に保存済みの
注意点
HTML への直接出力時のセキュリティ:
- この変更は「DB に保存する JSON の文字列の中身」がエスケープされなくなる、という変更です。
- Rails の view レイヤ (
<%= %>やraw,html_safeなど) での XSS 対策とは別物です。 - JSON をそのまま HTML に埋め込んでいるようなコード(例えば
<script>var data = <%= @model.settings.to_json %>;</script>のようなもの)は、元々も適切な HTML エスケープとjヘルパーなどによる JS エスケープを自前で考慮すべき領域です。この PR によってそこが安全になったり危険になったりするわけではありませんが、「DB に保存される値が変わる」ことは理解しておく必要があります。
テストの更新:
- 自前で
Coders::JSONのシリアライズ結果文字列を検証しているテストがある場合、"\u003c"を期待していた部分は"<"になるよう修正が必要です。
- 自前で
- 参考情報 (あれば)
関連するコンポーネント:
ActiveRecord::Coders::JSONActiveRecord::Type::Json- DB のネイティブ JSON 型(PostgreSQL の
json/jsonbなど)
この PR により、以下の 3 者の JSON シリアライズ結果が揃うことが期待されます:
- ネイティブ JSON カラム (
json,jsonb) ActiveRecord::Type::JsonActiveRecord::Coders::JSON(本 PR の対象)
- ネイティブ JSON カラム (
HTML エスケープに依存していたコードがないかだけ確認しつつ、基本的には「バグ修正 + 挙動の統一」として受け入れてよい変更です。
#57990 Match HTTP auth scheme case-insensitively for Token and Bearer
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
HTTP のAuthorizationヘッダにおけるToken/Bearer認証スキームの判定が、大文字小文字を区別しない(case-insensitive)形に修正されました。これにより、bearer abc123やTOKEN xyzといった非標準の大文字小文字でも正しくトークンが認識されるようになります。
- 変更内容の詳細
2-1. 変更の背景
- RFC 9110 §11.1 によると、
Authorizationヘッダ内のauth-scheme(Basic,Bearer,Tokenなどのスキーム名)は 大文字小文字を区別しない と規定されています。 - 現実のクライアントやプロキシは
Bearerだけでなくbearer,BEARERなどさまざまな表記で送ってくることがあります。 - しかし Rails では:
- コントローラ側の
Token認証 (ActionController::HttpAuthentication::Token) ActionDispatch::Request#bearer_token
がいずれも 大文字小文字を区別する 実装になっており、bearer abc123→ 無効として扱われ 401 を返すrequest.bearer_tokenもBearer以外の表記ではnilを返す
といった問題がありました。
- コントローラ側の
- 一方で、同じファイルにある
Basic認証はすでに case-insensitive で比較しており、振る舞いが不統一でした。
本 PR はこの不整合を解消し、RFC 準拠かつ Basic と同様に、Token / Bearer も case-insensitive に扱うようにしています。
2-2. コントローラ側 (Token 認証) の変更
対象:
actionpack/lib/action_controller/metal/http_authentication.rb
ActionController::HttpAuthentication::Token 内の TOKEN_REGEX が修正されています。
元から存在する TOKEN_REGEX は Authorization ヘッダをパースして Token スキームかどうかを判定するための正規表現ですが、スキーム部分を 大文字小文字を区別せずにマッチ するように変わりました。
イメージとしては、以下のような変更です(実際のコードからの擬似例):
# Before(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/ # "Token" と完全一致でマッチ
# After(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/i # "Token" を大文字小文字を区別せずにマッチこれにより、以下のようなヘッダがすべて同様にトークンとして認識されます:
Authorization: Token abc123
Authorization: token abc123
Authorization: TOKEN abc123
Authorization: ToKeN abc123コントローラ側で authenticate_or_request_with_http_token などを使っている場合、これら全てが有効なトークンとして扱われます。
2-3. リクエスト側 (Request#bearer_token) の変更
対象:
actionpack/lib/action_dispatch/http/request.rb
ActionDispatch::Request#bearer_token は、Authorization ヘッダから Bearer トークンを取り出すヘルパーメソッドです。
元は Bearer というスキーム名を 大文字小文字を区別して マッチさせていたため、以下のようなケースは全て nil を返していました:
Authorization: bearer abc123
Authorization: BEARER abc123
Authorization: Bearer abc123
# → Bearer 以外はトークンが取れないPR ではここも case-insensitive に変更され、Basic と同様の挙動になります。
擬似的なイメージ:
# Before(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/)
# After(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/i)これにより、以下のようなコードがすべて期待通りに動作します:
# 例: Rack ミドルウェアやコントローラ内
token = request.bearer_token
# Authorization: bearer abc123
# Authorization: BEARER abc123
# Authorization: Bearer abc123
# いずれも token == "abc123" になる2-4. テストの追加
対象:
actionpack/test/controller/http_token_authentication_test.rb(+9)actionpack/test/dispatch/request_test.rb(+5)
上記の動作が保証されるよう、以下の観点のテストが追加されています:
Token認証について、TOKEN,token,ToKeNなどさまざまな表記のAuthorizationヘッダを送っても正しく認証されること。Request#bearer_tokenについて、bearer,BEARERなど任意の大文字小文字の組み合わせでもトークンを抽出できること。
これにより、今後のリファクタリング等で再び case-sensitive に戻ってしまうリグレッションを防止します。
- 影響範囲・注意点
3-1. 影響範囲
- HTTP トークン認証 (
ActionController::HttpAuthentication::Token) を利用しているアプリケーション ActionDispatch::Request#bearer_tokenを直接、またはライブラリを通じて利用しているアプリケーション- 上記を利用しているライブラリ・ミドルウェア(OAuth2 実装、API 認証機構など)
基本的には後方互換な改善 であり、従来通り Token xxx / Bearer xxx もそのまま動作します。そのうえで、今まで 401 になっていた bearer xxx / TOKEN xxx などが通るようになります。
3-2. 注意点・想定外の影響になり得るケース
- もしアプリケーション側で「
Bearerという表記だけを許したい」「tokenは拒否したい」といった、あえて非 RFC 準拠な制約 を設けていた場合、その前提は崩れます。
ただし、auth-schemeを case-sensitive に扱うこと自体が RFC 9110 に反するため、そのような前提がある場合は設計の見直しが推奨されます。 - 同一クライアント・同一トークンで、これまでたまたま
Bearerになっていたケースでは何も変わらず、混在環境(クライアントの一部がbearerを送るなど)でのみ挙動が変わる可能性があります。
- 参考情報 (あれば)
- RFC 9110 §11.1: Authentication Scheme
auth-schemeは case-insensitive と定義されている。
- 関連する Rails コード:
ActionController::HttpAuthentication::Tokenactionpack/lib/action_controller/metal/http_authentication.rbActionDispatch::Request#bearer_tokenactionpack/lib/action_dispatch/http/request.rb
- 類似の既存挙動:
Basic認証はすでにスキーム名を case-insensitive に扱っており、本 PR によってToken/Bearerもそれに揃えられた形になります。
#57993 Fix incorrect number_to_percentage default precision in docs [ci skip]
マージ日: 2026/7/4 | 作成者: @anxkhn
- 概要 (1-2文で)
number_to_percentageヘルパーのドキュメントに書かれていた:precisionオプションのデフォルト値が誤っており、実際の挙動(デフォルト3桁)に合わせて「2」から「3」に修正した PR です。コードや挙動の変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
何が問題だったか
number_to_percentage の RDoc には、:precision オプションについて次のように書かれていました。
# [+:precision+]
# The level of precision, or +nil+ to preserve +number+'s precision.
# Defaults to 2.
#
# number_to_percentage(12.3456789, precision: 4) # => "12.3457%"しかし実際には、number_to_percentage(100) のデフォルト出力は "100.000%" であり、デフォルト精度は「3」です。
- ベースのフォーマット設定:
DEFAULTS[:format][:precision] = 3 :currencyだけが precision を 2 にオーバーライド:percentageは:delimiterと:formatしかオーバーライドしていないため、precision=3 を継承
そのため、「Defaults to 2.」という説明が実際の挙動および直前のサンプル (number_to_percentage(100) # => "100.000%") やテストと矛盾していました。
具体的な修正内容
activesupport/lib/active_support/number_helper.rb の1行だけが変更されています。
# [+:precision+]
# The level of precision, or +nil+ to preserve +number+'s precision.
-# Defaults to 2.
+# Defaults to 3.
#
# number_to_percentage(12.3456789, precision: 4) # => "12.3457%"number_to_currency 側の「Defaults to 2.」は正しいためそのままです。
- 影響範囲・注意点
実行時の挙動は一切変わりません。
もともとnumber_to_percentageのデフォルト precision は 3 であり、この PR はそれを説明するドキュメントを正しくしただけです。影響があるのは「ドキュメントを読んで実装していた人の認識」だけです。
- これまで「precision のデフォルトは 2 だ」と思い込んでいた場合、実際の出力とのギャップに気づいていなかった可能性があります。
- ただしコード側は以前から 3 桁出力で動いており、PR による挙動変更はありません。
もしアプリ側で「パーセンテージは必ず小数2桁にしたい」という要件がある場合は、明示的に precision を指定する必要があります。
rubynumber_to_percentage(12.3456) # => "12.346%" (デフォルト precision: 3) number_to_percentage(12.3456, precision: 2) # => "12.35%"他のヘルパーとの一貫性:
number_to_rounded,number_to_human_size,number_to_humanなど、同じくベースの precision を継承するヘルパーはすでに「Defaults to 3.」と記載されており、number_to_percentageだけがドキュメント上ズレていました。本 PR により説明の一貫性が取れています。
- 参考情報 (あれば)
デフォルト precision が 3 であることを示すソース:
activesupport/lib/active_support/number_helper/number_converter.rbDEFAULTS[:format][:precision] = 3DEFAULTS[:currency][:format][:precision] = 2DEFAULTS[:percentage]は:delimiterと:formatだけ上書きし、precision は継承
activesupport/lib/active_support/locale/en.ymlnumber.format.precision: 3number.currency.format.precision: 2number.percentage.formatは precision 未指定 → 3 を継承
テストコード
activesupport/test/number_helper_test.rbactivesupport/test/core_ext/numeric_ext_test.rb
いずれもnumber_to_percentage(100) == "100.000%"を前提にしており、precision=3 を前提にしていることが分かります。
#57986 Fix Guides Index flyout menu overflowing its panel
マージ日: 2026/7/4 | 作成者: @iuhoay
- 概要 (1-2文で)
Rails Guides のデスクトップ向け「Guides Index」フライアウトメニューで、3カラムレイアウトが横幅を超えて右にはみ出していた問題を、CSS のレイアウト指定を見直すことで解消した PR です。widthとmarginの組み合わせで 100% を超えていたものを、column-gapとcalc()を用いてちょうど 100% に収まるようにしています。
- 変更内容の詳細
対象: guides/assets/stylesrc/_main.scss
問題点:
.guides-section-container内の.guides-sectionが以下のように指定されていました:width: 33%;margin: 0 2em 0.5em 0;
- 3カラム × 33% = 99% に加え、各カラム右側に
2emマージンがあり、その分が合計で 100% を超えてしまい、右端のカラムがコンテナの外にはみ出していました。 - はみ出した結果、右端カラム内の長いリンクテキスト(例: "Installing Rails Core Development Dependencies")がパネル外へ溢れて見えてしまう状態になっていました。
修正内容:
.guides-section-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
+ column-gap: 2em;
max-height: 60em;
width: 100%;
.guides-section {
flex: auto;
- margin: 0 2em 0.5em 0;
+ margin: 0 0 0.5em 0;
text-align: left;
- width: 33%;
+ width: calc((100% - 4em) / 3);
}
}ポイント:
- カラム間のスペースの扱いを、各要素の
margin-rightではなく、コンテナ側のcolumn-gapに移行:- 以前:
.guides-sectionにmargin-right: 2em;相当の指定 - 以後:
.guides-section-containerにcolumn-gap: 2em;
- 以前:
.guides-sectionの幅を固定の33%から、カラム間ギャップを考慮したcalc((100% - 4em) / 3)へ変更:- 3カラムとその間に 2つのギャップ(
2em× 2 =4em)がある前提で、- コンテナの 100% 幅からギャップ合計
4emを引いた残りの幅を 3 等分。
- コンテナの 100% 幅からギャップ合計
- これにより「カラムの幅 × 3 + ギャップ × 2 = 100%」となり、パネル幅を超えなくなります。
- 3カラムとその間に 2つのギャップ(
.guides-sectionのmarginは下方向 (margin-bottom: 0.5em) のみ残し、左右の余白は0に変更。
結果として、3カラムとその間の余白がコンテナ幅にぴったり収まり、右端カラムや長いリンクテキストがパネル外へ溢れなくなります。
- 影響範囲・注意点
- 影響範囲:
- Rails Guides サイトのデスクトップビューにおける「Guides Index」フライアウトメニューのレイアウトに限定されます。
- それ以外のページレイアウトや、Rails 本体の機能・挙動には影響しません(スタイルシートのみの変更)。
- 技術的な注意点:
column-gapは比較的新しめの CSS プロパティですが、デスクトップ主要ブラウザでは概ねサポートされています。Rails Guides のターゲットブラウザを考えると問題になる可能性は低いと考えられます。calc((100% - 4em) / 3)によって、コンテナ幅とフォントサイズ(emに依存)が変わるとカラム幅も自動的に再計算されるため、レイアウトはある程度柔軟に対応できますが、「3カラム+ギャップ 2つ」であることが前提になっています。将来的にカラム数を変える場合は、この式も合わせてメンテナンスする必要があります。flex-direction: column; flex-wrap: wrap;という少し特殊な使い方をしているため、DOM 構造の変更やコンテナ高さの変更(max-height: 60em;)とあわせて動作を確認するのが望ましいです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57986
- CSS リファレンス:
- Flexbox と縦方向ラップ(
flex-direction: column; flex-wrap: wrap;)の挙動:
https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap
#57997 Use Range#cover? for this_week?/this_month?/this_year?
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
Date/Time関連のヘルパーthis_week?/this_month?/this_year?が、内部判定にRange#include?ではなくRange#cover?を使うように変更され、性能が大きく改善されました。振る舞いは変えずに、呼び出しごとの無駄な日付走査を避けるリファクタリングです。
- 変更内容の詳細
何をしているか
this_week?, this_month?, this_year? は、引数の日時オブジェクトが「今週 / 今月 / 今年」に属するかどうかを判定するメソッドです。
もともとはだいたい次のようなイメージで実装されていました:
def this_week?
all_week.include?(self)
end
def this_month?
all_month.include?(self)
end
def this_year?
all_year.include?(self)
endここで all_week, all_month, all_year は Date の Range を返します。Range#include? は #succ で一つずつ値を進めながら探索する実装になるため、Date の場合:
this_week?→ 約 7 回succ(7日分)this_month?→ 最大 31 回succthis_year?→ 約 365 回succ
といった線形探索が毎回発生していました。
今回の変更では、これらを Range#cover? に差し替えています:
def this_week?
all_week.cover?(self)
end
def this_month?
all_month.cover?(self)
end
def this_year?
all_year.cover?(self)
endRange#cover? は「範囲の開始と終了の比較」だけで判定するため、内部で要素を1つずつたどることがなく、10〜100倍程度高速になることが PR 説明で言及されています。
背景
- 先行 PR #57963 で
this_quarter?はすでにcover?に変えられており、今回の PR はそのフォローアップとして、兄弟関数 (this_week?/this_month?/this_year?) を同じ実装方針に揃えたものです。 - テストの変更はなく、既存の境界条件テストもそのまま通ることが確認されています。
- 影響範囲・注意点
挙動面
Range#include?とRange#cover?は、Date/Timeのように比較が定義されたオブジェクトに対して、典型的な連続範囲を扱う場合は同じ結果を返します。- 既存テストが境界値(開始日・終了日を含むかどうか等)をカバーしており、そのままパスしているため、APIの論理的挙動は変わらないと見なして問題ありません。
パフォーマンス面
this_year?のように長い期間の Range を頻繁に判定しているコードでは、顕著な性能改善が見込めます。- 特に大量レコードに対して
this_*?を呼ぶ場面(バリデーション、集計、バッチ処理など)で恩恵が大きくなります。
後方互換性
- 公開 API のメソッドシグネチャや返り値の型に変更はなく、内部実装の最適化のみです。
Range#cover?/Range#include?の差異に依存した特殊なケース(例えば、離散的でない独自オブジェクトを Range に入れている等)をこのメソッド群が利用しているわけではないので、互換性上のリスクは非常に低いと考えられます。
- 参考情報 (あれば)
- この PR: https://github.com/rails/rails/pull/57997
- 先行 PR (
this_quarter?をcover?に変更): https://github.com/rails/rails/pull/57963 - Ruby リファレンス(
Range#cover?とRange#include?の違い)cover?: 始点・終点の比較で判定(O(1))include?/member?:===判定・succによる走査の可能性があり、離散オブジェクトでは O(n) になり得る
#57963 Add this_quarter? to Date/Time
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
Date,Time,DateTime,ActiveSupport::TimeWithZoneに対して「今期(今四半期)かどうか」を判定するthis_quarter?メソッドが追加されました。これにより、week/month/year に揃う形で quarter にもthis_*?プレディケートが用意され、日付処理の一貫性が向上しています。
- 変更内容の詳細
追加された機能
対象クラス:
DateTimeDateTimeActiveSupport::TimeWithZone
に、以下のプレディケートメソッドが追加されました:
date_or_time.this_quarter? # => true / false判定ロジックの概要this_month? や this_year? と同様に、「現在の四半期」を基準にして判定します。
- 基準となる“今”は
Date.current(=Time.zoneを考慮した現在日付) - その
Date.currentが属する四半期を「現在の四半期」とみなし、 - 受け取った日付/時刻オブジェクトがその四半期の範囲内にあるかどうかを判定
概念的には以下と同等の判定です:
def this_quarter?
Date.current.all_quarter.cover?(to_date)
end※実装は activesupport/lib/active_support/core_ext/date_and_time/calculations.rb に追加されており、既存の this_month? / this_year? と同じスタイルで this_quarter? が定義されています。
使用例
# 前提: Date.current が 2000-02-15 (Q1: 1〜3月) の場合
Date.new(2000, 3, 31).this_quarter? # => true (同じ四半期 Q1)
Date.new(2000, 4, 1).this_quarter? # => false (次の四半期 Q2)
Time.utc(2000, 1, 1).this_quarter? # => true (Q1 に含まれる)
DateTime.civil(2000, 4, 1).this_quarter? # => false (Q2)既存機能との関係
Rails には既に以下のような四半期関連のメソッドがありました:
beginning_of_quarterend_of_quarterall_quarternext_quarterprev_quarter
一方で、週・月・年にはある this_*? 系が四半期にだけ存在していませんでした:
| week | month | quarter | year | |
|---|---|---|---|---|
| beginning_of_* | ✓ | ✓ | ✓ | ✓ |
| end_of_* | ✓ | ✓ | ✓ | ✓ |
| all_* | ✓ | ✓ | ✓ | ✓ |
| next_* | ✓ | ✓ | ✓ | ✓ |
| prev_* | ✓ | ✓ | ✓ | ✓ |
| this_*? | ✓ | ✓ | — | ✓ |
今回の PR で、この — が this_quarter? により埋まり、API の一貫性が取れるようになりました。
テストとドキュメント
変更ファイル:
activesupport/lib/active_support/core_ext/date_and_time/calculations.rbthis_quarter?実装追加 (+5行)
activesupport/test/core_ext/date_time_ext_test.rbtest_this_quarter追加 (+9行)this_month?/this_year?と同じテストパターンで、四半期境界をカバー
activesupport/CHANGELOG.md- 新機能として
this_quarter?追加を明記 (+12行)
- 新機能として
テストでは、四半期の開始日・終了日・直前直後の日付などを用い、境界条件を確認しています。
- 影響範囲・注意点
影響範囲
- ActiveSupport の
Date/Time拡張を利用しているアプリケーション全般で、新たにthis_quarter?が利用可能になります。 - 既存メソッドの挙動変更や破壊的変更は含まれていません(追加のみ)。
実務上の利点
これまでよく見られたコード:
Date.current.all_quarter.cover?(record.created_at.to_date)
# または
Date.current.all_quarter.include?(record.created_at.to_date)を、より簡潔に:
record.created_at.this_quarter?と書けるようになります。
四半期ベースの:
- 請求期間・課金期間の判定
- OKR・目標管理の「今期」判定
- 財務・レポート系でのフィルタリング
などが読みやすく実装できます。
Time.zone / タイムゾーンへの注意
this_quarter? は Date.current を基準に判定するため、config.time_zone と Time.zone の設定に依存します。this_month? / this_year? と同じ挙動です。
例:
- アプリの
Time.zoneが"Tokyo"の場合: 「今期」は東京時間での日付を基準とする - DB が UTC でも、
TimeWithZoneで評価される場合はTime.zoneを考慮
タイムゾーンを切り替えるテストや、マルチタイムゾーン対応アプリでは、Time.use_zone を用いた検証を行うと安全です。
- 参考情報 (あれば)
- PR 本文(英語)でのディスカッションと動機:
- 事前議論が行われた Rails forum:
- 既存の
this_month?/this_year?実装の参考:activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
#57987 Make add_index(if_not_exists: true) reversible
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
add_index/remove_indexのマイグレーションを逆変換(rollback 時の自動生成)する際に、if_not_exists/if_existsオプションが正しく対応付けられるようになりました。これにより、既存/非存在なインデックスに対して安全にロールバックでき、これまで発生していた不要な例外が解消されます。
- 変更内容の詳細
何が問題だったか
Rails の Migration には「コマンドインバータ(ActiveRecord::Migration::CommandRecorder)」があり、change メソッド内で書いた操作から rollback 用の逆操作を自動的に生成します。
しかし、これまで以下のような問題がありました:
remove_index(..., if_exists: true)を反転するとadd_indexが生成されるが、
その際にif_exists: trueが 失われる(:if_exists は add 側には存在しないオプションのため、単に削除されるだけだった)。- 結果: rollback 時に、すでにインデックスが存在するケースで例外が発生する。
add_index(..., if_not_exists: true)を反転するとremove_indexが生成されるが、if_not_exists: trueがそのままremove_indexに渡されてしまい、remove_indexはif_not_existsを 理解しない ため、事実上オプションが無視される。- 結果: rollback 時に、インデックスが存在しないケースで例外が発生する。
つまり、「forward 方向では存在チェック付きで安全に動くように書いたマイグレーション」が、rollback 時にはその安全性が失われていた、という状態です。
今回の修正内容
CommandRecorder の invert_add_index / invert_remove_index において、if_not_exists / if_exists を相互に変換するようになりました。
概念的には以下のような変換が行われます:
# 変更前(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], {}] # :if_exists が落ちる
invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_not_exists: true }] # remove_index では無視される
# 変更後(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], { if_not_exists: true }]
invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_exists: true }]これにより:
- 「存在していたら削除」を rollback すると「存在していなかったら作成しない(存在しなくてもエラーにしない)」という安全な対になる関係になる
- 「なければ作らない」を rollback すると「すでになくてもエラーにしない削除」になる
サンプルコードイメージ
class AddIndexToUsersEmail < ActiveRecord::Migration[7.2]
def change
# インデックスがなければ作成
add_index :users, :email, if_not_exists: true
end
endこの change を rollback すると、従来は概ねこう解釈されていました:
remove_index :users, :email
# if_not_exists は無視されるので、インデックスがなければエラー今回の修正後は、内部的には以下のような remove_index が生成されます:
remove_index :users, :email, if_exists: true
# インデックスがなくてもエラーにならない逆に:
class RemoveIndexFromUsersEmail < ActiveRecord::Migration[7.2]
def change
remove_index :users, :email, if_exists: true
end
endを rollback すると、今までは(概念的に):
add_index :users, :email
# すでにインデックスがあるとエラーとなっていましたが、これからは:
add_index :users, :email, if_not_exists: true
# すでにインデックスがあってもエラーにならないとなります。
実装面の変更
activerecord/lib/active_record/migration/command_recorder.rbadd_index/remove_indexの invert ロジックに、オプションハッシュの変換処理を追加:if_not_exists→:if_exists、:if_exists→:if_not_existsのマッピングを行うようにした
activerecord/test/cases/migration/command_recorder_test.rb- 上記の変換が正しく行われているかどうかを検証するテストを追加
この挙動は前にマージされた invert_add_foreign_key / invert_remove_foreign_key(PR #57972)や check_constraint 周りの invert 処理と揃えた形です。
- 影響範囲・注意点
影響範囲
changeメソッド内でadd_index ... if_not_exists: trueまたはremove_index ... if_exists: trueを利用しているマイグレーションの rollback 挙動が、より安全かつ期待に沿ったものになります。- 既存のマイグレーションファイルに対しても、Rails 本体を上げれば新しい CommandRecorder のロジックで rollback が行われます。
期待されるメリット
- すでに適用済みのマイグレーションを本番・ステージングで rollback する際に、「インデックスがもうある / もうない」状況でも例外が出づらくなります。
- 並列デプロイや手動メンテなどでインデックス状態がずれがちな環境でも、マイグレーションの安全性が増します。
注意点
- 動作が「壊れる」方向ではなく、「これまで例外が出ていたケースで出なくなる」変更なので、互換性上の問題はほぼありません。
- ただし、あえて rollback 時にインデックスの存在 / 非存在を厳密に検出したい、という特殊なケースでは挙動が変わる(例外が起きにくくなる)点は認識しておくとよいです。
- Schema の整合性チェック等で、インデックスの存在を前提にした独自ツールを組んでいる場合は、「if_not_exists / if_exists 付きのマイグレーションでも rollback が失敗しにくくなった」ことを踏まえて運用を見直す余地があります。
- 参考情報 (あれば)
- この PR:
- Make
add_index(if_not_exists: true)reversible (#57987)
- Make
- 関連 PR:
- foreign key inverter を同様に修正した PR: #57972
- 該当クラス/メソッド:
ActiveRecord::Migration::CommandRecorder#invert_add_indexActiveRecord::Migration::CommandRecorder#invert_remove_index
#57992 Require fileutils in Cache::FileStore
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::FileStore内でFileUtilsを利用しているにもかかわらず"fileutils"を require していなかった問題を修正し、スタンドアロンな ActiveSupport 利用時に発生していたNameErrorを解消する PR です。Rails フルスタックでは暗黙に読み込まれていた依存を、明示的に宣言するようになりました。
- 変更内容の詳細
変更は 1 行のみで、ActiveSupport::Cache::FileStore のファイル先頭付近に require "fileutils" が追加されました。
イメージとしては以下のような変更です(擬似コード):
# activesupport/lib/active_support/cache/file_store.rb
# 旧:
# module ActiveSupport
# module Cache
# class FileStore < Store
# ...
# FileUtils.makedirs(...)
# FileUtils.rm_r(...)
# ...
# 新: fileutils を明示的に読み込む
require "fileutils"
module ActiveSupport
module Cache
class FileStore < Store
# 内部で FileUtils.makedirs, FileUtils.rm_r を使用
end
end
end背景として、このクラスはキャッシュディレクトリの作成や削除に FileUtils.makedirs や FileUtils.rm_r を使っていますが、これまでは同じプロセス内のどこか別の場所で "fileutils" が require されている前提になっていました。Rails 全体をロードしている場合は、他のコンポーネントが fileutils を読み込むため問題は表面化していませんでしたが、ActiveSupport 単体で ActiveSupport::Cache::FileStore を使うと次のようなエラーが起きていました。
require "active_support"
require "active_support/cache"
store = ActiveSupport::Cache::FileStore.new("/tmp/cache")
store.write("foo", "bar")
# => NameError: uninitialized constant ActiveSupport::Cache::FileStore::FileUtilsこの PR により、file_store.rb 自身が require "fileutils" するため、上記のような NameError は発生しなくなります。
- 影響範囲・注意点
影響範囲
ActiveSupport::Cache::FileStoreを利用する全てのコードに対し、挙動は「例外が出なくなる」以外は実質的な変更はありません。- 既に Rails フルスタックを利用しているアプリケーションでは、もともとどこかで
fileutilsがロードされていたため、挙動上の違いはほぼありません。 - ActiveSupport を単体で利用しているツール・スクリプト・ライブラリにおいて、
FileStoreを使った際のNameError (uninitialized constant FileUtils)が解消されます。
注意点
require "fileutils"は Ruby 標準ライブラリの読み込みであり、互換性上の問題を引き起こす可能性は低いです。- もしアプリ側で
FileUtils定数を独自にモンキーパッチしていた場合でも、この PR は単に標準ライブラリを require するだけなので、通常は影響ありません(既にロード済みなら再 require も安全)。 - 「他のファイルに依存して暗黙に定数をロードする」状態が解消され、今後は
file_store.rb単体で完結した依存関係になります。
- 参考情報 (あれば)
- 対象クラスのドキュメント(キャッシュストアの概要など)
- ActiveSupport::Cache::FileStore (現行 Rails ガイド / RDoc)
- 類似のパターンとして、標準ライブラリに依存しているクラス・モジュールは、そのファイル自身で
requireするのが推奨されます。今回の修正は、そのベストプラクティスに沿ったものといえます。
#57984 Quote the index name in MySQL enable_index/disable_index
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
MySQL / MariaDB 用アダプタのenable_index/disable_indexが、インデックス名をクォートせずに SQL を生成していた問題を修正し、予約語や特殊文字を含むインデックス名でも正常に動作するようにしました。rename_indexなど他のインデックス操作と一貫して、インデックス名にquote_column_nameを適用するようになっています。
- 変更内容の詳細
何が問題だったか
enable_index / disable_index は、内部的に MySQL / MariaDB の以下のような SQL を発行します:
ALTER TABLE `testings` ALTER INDEX order VISIBLE
ALTER TABLE `testings` ALTER INDEX order INVISIBLEここで order のように、
- SQL の予約語
- バッククォートすべき特殊文字を含む名前
をインデックス名に使っていると、インデックス名がクォートされていないため 構文エラー になります。
どのように修正されたか
abstract_mysql_adapter.rb の enable_index / disable_index 実装で、インデックス名を quote_column_name で包むように変更されました。
概念的には以下のような変更です:
# 変更前 (イメージ)
def enable_index(table_name, index_name)
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} VISIBLE")
end
# 変更後 (イメージ)
def enable_index(table_name, index_name)
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} VISIBLE")
end
def disable_index(table_name, index_name)
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} INVISIBLE")
endその結果、実際に発行される SQL は:
-- Before
ALTER TABLE `testings` ALTER INDEX order VISIBLE
-- After
ALTER TABLE `testings` ALTER INDEX `order` VISIBLEとなり、インデックス名がバッククォートで囲まれます。
quote_column_name は、MySQL アダプタではカラム名・インデックス名など識別子を `name` のようにクォートするためのメソッドで、既に rename_index など他のインデックス操作では使われていました。今回の修正で、インデックス関連操作のクォート戦略が統一されました。
テストの追加
activerecord/test/cases/migration/index_test.rb にテストが追加され、
- 予約語や特殊文字を含むインデックス名に対して
enable_index/disable_indexがエラーなく動作し、正しい SQL が生成される
ことが検証されています。
(具体的なテストコードは PR 情報にはありませんが、order のようなケースをカバーしていると考えられます。)
- 影響範囲・注意点
- 対象:
- Active Record を使った MySQL / MariaDB アプリケーション
enable_index/disable_indexを直接またはマイグレーション経由で利用している場合
- 影響:
- これまで「インデックス名が予約語等だと
enable_index/disable_indexで落ちる」ケースが解消されます。 - 既存の正常なケースについては、単にインデックス名がクォートされるだけなので、後方互換性への影響はほぼありません(MySQL / MariaDB は識別子をクォートしても同じオブジェクトを指すため)。
- これまで「インデックス名が予約語等だと
- 注意点:
- もしアプリ側で生 SQL を組み立てて
ALTER TABLE ... ALTER INDEX ...を発行している場合は、この修正は関係ありません。その場合も同様に、インデックス名はバッククォートでクォートするのが安全です。 - マイグレーション上で
index: { name: "order" }のような名前を指定しても、今回の修正によりenable_index/disable_index時のエラーは防げるようになりますが、アプリケーションコードや他の SQL からの参照でも予約語を使う場合は同様の注意が必要です。
- もしアプリ側で生 SQL を組み立てて
- 参考情報 (あれば)
- MySQL マニュアル:
- 「Schema Object Names」(識別子とクォート)
- 「Reserved Words」(予約語一覧)
- Rails ソース周辺:
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#quote_column_namerename_index実装 (同アダプタ内、インデックス名クォートの既存の扱いの参考になる)
#57988 Raise on ignored if_exists option in change_table's remove_timestamps
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
change_tableブロック内でt.remove_timestampsにif_exists:/if_not_exists:オプションを渡した場合、これまでは黙って無視されていましたが、他のメソッドと同様にArgumentErrorを投げて呼び出し側に誤用を知らせるように変更されました。これにより、サポートされないオプションの利用を早期に検知できるようになります。
- 変更内容の詳細
何が変わったか
- 対象メソッド:
ActiveRecord::ConnectionAdapters::Table#remove_timestamps - 変更点:
remove_timestampsが内部でraise_on_if_exist_optionsを呼ぶようになりました。- これにより、
if_exists:/if_not_exists:オプションが渡された場合は例外を発生させます。
従来の挙動:
change_table :users do |t|
t.remove_timestamps if_exists: true # ← オプションは黙って無視される
end
# 例外は発生しないが、if_exists: は一切効いていない今回の変更後:
change_table :users do |t|
t.remove_timestamps if_exists: true
end
# => ArgumentError: options :if_exists, :if_not_exists are not supported ...これは、既に以下のようなメソッドで行われている挙動と揃えたものです。
t.timestamps(if_exists: true)→ArgumentErrort.remove(:column, if_exists: true)→ArgumentErrort.remove_index(:column, if_exists: true)→ArgumentError
つまり、change_table ブロック内の「if_exists / if_not_exists をサポートしないヘルパー」は、全て一貫して例外を投げるようになりました。
テスト追加
activerecord/test/cases/migration/change_table_test.rb にテストが追加され、例えば以下のようなケースがカバーされていると考えられます。
t.remove_timestamps(if_exists: true)がArgumentErrorを投げることt.remove_timestamps(if_not_exists: true)がArgumentErrorを投げること
(実際のテストコードは数行で、この挙動を保証する内容になっています。)
- 影響範囲・注意点
影響を受けるケース
- 既存のマイグレーションで、
change_tableブロック内に以下のようなコードがある場合:
change_table :users do |t|
t.remove_timestamps if_exists: true
# または
t.remove_timestamps if_not_exists: true
endこれらは これまでは正常終了していたが、オプションは効いていなかった のに対し、今後は ArgumentError によってマイグレーションが失敗します。
なぜ例外にするのか
- サポートされていないオプションを黙って無視すると、開発者が「存在しない場合はスキップされるはず」と誤解したままになり、スキーマ不整合や本番環境でのバグの原因になります。
- 他の
change_tableヘルパー (timestamps,remove,remove_indexなど) はすでに同じポリシーで挙動しており、remove_timestampsだけ例外だったため、API 一貫性の観点からも修正されています。
対応方法
もし上記のようなコードが既に存在する場合は:
if_exists:/if_not_exists:を削除する (最も単純):rubychange_table :users do |t| t.remove_timestamps end「存在チェック込みで安全に消したい」という意図であれば、自前で条件分岐する:
rubydef up if column_exists?(:users, :created_at) && column_exists?(:users, :updated_at) change_table :users do |t| t.remove_timestamps end end end def down change_table :users do |t| t.timestamps end end
なお、
change_tableではなくremove_timestampsを直接呼び出すremove_timestamps(:users)のような DSL を使っている場合、この PR はchange_tableブロック内のt.remove_timestampsに対する変更なので、その部分には直接は影響しません (この PR の差分範囲に依存しますが、説明文からはchange_tableブロック内のヘルパーが対象と読み取れます)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57988
- 関連する API:
change_tableブロック内ヘルパー:t.timestampst.removet.remove_indext.remove_timestamps
- オプション検証メソッド:
raise_on_if_exist_options(ActiveRecord 内部ユーティリティ)
この変更は「サイレントに失敗するマイグレーション」を減らすための一貫性改善であり、バグ修正に近い性質の変更と考えられます。
#57989 Fix color_field emitting invalid color values for strings containing a hex substring
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
color_fieldヘルパーの色値バリデーションが不正確だったため、<input type="color">にブラウザが解釈できない値がそのまま出力されていた問題を修正しています。正規表現を「文字列全体が#rrggbb形式かどうか」を見るように変更し、それ以外は#000000にフォールバックする挙動を統一しました。
- 変更内容の詳細
何が問題だったか
従来の color_field 内の validate_color_string は、値の中に「6桁の16進数」がどこかに含まれていれば OK と判定していました。
そのため、例えば以下のような値が「有効」とみなされていました。
"Not a color #123456 at all""#12345678"(8桁、実際には CSS の#rrggbbaaのような形式)- その他、文字列の一部に
#abcdefのようなパターンが含まれるもの
これらはそのまま <input type="color"> の value 属性に出力されるため、ブラウザ側では無効なカラー値として扱われ、期待どおり動作しない(色が選択されていない状態になるなど)問題がありました。
具体的な修正内容
validate_color_string の正規表現を、
- 「6桁の 16 進数の連続部分をどこかから拾ってくる」
から - 「文字列全体が
#rrggbb形式と完全一致しているかを確認する」
ように変更しています。
Ruby 的には、例えばこんなイメージです(実際のコード断片のイメージ):
def validate_color_string(value)
# 変更前(イメージ)
# value[/#[0-9a-fA-F]{6}/] || "#000000"
# 変更後(イメージ)
value =~ /\A#[0-9a-fA-F]{6}\z/ ? value : "#000000"
end/\A...\z/ でアンカーしているため、以下のような挙動になります:
"Not a color #123456 at all"→ 不一致 →"#000000"にフォールバック"#12345678"→ 不一致 →"#000000"にフォールバック"#1234TR"→ 不一致 →"#000000"(従来からのフォールバック挙動を踏襲)"#123456"→ 一致 → そのまま"#123456"を利用
テストの追加
actionview/test/template/form_helper_test.rb に 6 行ほどテストが追加され、上記のようなケースで:
- 部分一致する文字列や
- 8桁のカラーや
- その他の malformed な値
が渡されたときに、value="#000000" が出力されることが確認されています。
- 影響範囲・注意点
影響範囲
ActionView::Helpers::FormTagHelper#color_field_tagActionView::Helpers::FormHelper#color_field
など、Rails 標準のcolor_field/color_field_tagを通じて<input type="color">を生成している部分すべてに影響します。
挙動の変化
- これまでは「文字列の中に有効な
#rrggbbパターンが一部でも含まれていれば、それをそのまま value に出力する」挙動でした。 - 今後は「文字列全体が
#rrggbb形式である場合だけ受け入れ、それ以外は必ず#000000にフォールバック」になります。 - ブラウザ上の見た目として、今まで一見「謎の初期値」になっていたケースが、明示的に黒 (
#000000) で表示されるようになります。
- これまでは「文字列の中に有効な
後方互換性の観点
- もしアプリケーション側で、意図的に
"Some text #ff0000"のような値をcolor_fieldに渡し、ブラウザが無効値として扱うことに依存していた場合、その挙動は変わります(常に#000000になります)。 - 一般的には、HTML の仕様に沿ったより厳格なバリデーションとなるため、バグ修正としての互換性の破壊は最小です。
#rrggbbaa(8桁)のような色表現を将来的に扱いたい場合は、color_fieldではなくカスタムの input / JS を利用する必要があります(HTML<input type="color">自体が現状#rrggbbしか受けないため)。
- もしアプリケーション側で、意図的に
注意点
- サーバー側から
color_fieldに渡す値は、#+ 16進6桁の文字列(/\A#[0-9a-fA-F]{6}\z/)に正規化してから渡すようにしておくと、今回の変更に依存せず安全です。 - 不正な値をそのまま投げても自動で
#000000になるため、UI 的に「入力値がおかしい」ことがユーザーに分からない可能性があります。必要に応じてモデルバリデーションやクライアント側バリデーションで補完してください。
- サーバー側から
- 参考情報 (あれば)
- HTML 仕様上、
<input type="color">のvalueは#rrggbb形式(#+ 16 進 6 桁)以外は無効値として扱われます。 - Rails 側でのフォールバックポリシー:
- 有効な
#rrggbb形式 → そのまま出力 - それ以外 →
#000000(黒)に統一
という仕様が、この修正でより一貫した形になりました。
- 有効な
#57985 Update PostgreSQL database.yml sample in Command Line guide [ci skip]
マージ日: 2026/7/4 | 作成者: @fuentesjr
- 概要 (1-2文で)
Command Line ガイド内の PostgreSQL 用database.ymlサンプルを、現行のrails new ... --database=postgresqlが生成する内容に合わせて更新した PRです。合わせて、非推奨になりつつあるpoolキーの例を、新しいmax_connectionsキーの例に置き換えています。
- 変更内容の詳細
対象ファイル: guides/source/command_line.md の「Configure a Different Database」セクション内の PostgreSQL サンプル config/database.yml。
主な変更は2点です。
(1) pg_config のパスの更新
Homebrew でインストールした pg gem に対して pg_config の場所を指定するコメントを、Intel Mac 前提のパスから Apple Silicon 前提のパスに変更しています。
-# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# gem install pg -- --with-pg-config=/opt/homebrew/bin/pg_configRails のジェネレータテンプレート(railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt)側が、すでに /opt/homebrew に切り替わっていたため、ガイドのサンプルもそれに追随させた形です。
(2) pool から max_connections への置き換え
database.yml 内の接続数関連のキー例を、旧来の pool から max_connections に変更しています。
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
+ max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>ポイント:
- キー名が
pool→max_connectionsに変更poolは、今後max_connections/min_connectionsに置き換わっていく方向で、ドキュメント上も新しいキーを教えるように修正されています。
- デフォルト値も
3→5に変更- これはジェネレータテンプレートに合わせたもので、実際に
rails new app --database=postgresqlした結果と一致させています。
- これはジェネレータテンプレートに合わせたもので、実際に
この変更により、ガイドに載っている YAML サンプルと、最新の Rails が自動生成する config/database.yml の内容が揃いました。
- 影響範囲・注意点
- 実装コードへの影響はなく、ドキュメントのみの変更です。
- ただし、これからガイドを読んで PostgreSQL 設定を行う開発者は、以下の点に注意が必要です:
- 新規:
- 新規アプリを Rails ジェネレータで作る場合、
pool:ではなくmax_connections:を使うのが推奨される形になります。 - コネクションプールサイズを調整したい場合は
max_connections/min_connectionsを前提に考えるとよいです。
- 新規アプリを Rails ジェネレータで作る場合、
- 既存アプリ:
- 既存アプリで
pool:を使っていても、この PR 自体が挙動を変えるわけではありません。 - ただし、今後のガイドや設定例は
max_connectionsベースになるため、タイミングを見て移行を検討すると、将来の非推奨・削除対応がしやすくなります。
- 既存アプリで
- 新規:
- Apple Silicon Mac(
/opt/homebrew)以外の環境では、pg_configのパスは当然異なり得ます:- Intel Mac(Homebrew):
/usr/local/bin/pg_config - Linux (例):
/usr/bin/pg_configなど
→ コメントはあくまで例示なので、自分の環境に合わせてパスは調整する必要があります。
- Intel Mac(Homebrew):
- 参考情報 (あれば)
- テンプレート本体:
railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.ttmax_connections/min_connectionsのドキュメント化に関する関連 PR:- #57736
- 対象ガイド(edge guides):
- Command Line > Configure a Different Database
https://edgeguides.rubyonrails.org/command_line.html#configure-a-different-database
- Command Line > Configure a Different Database
#57921 Expand Action Mailer delivery_method configuration docs [ci skip]
マージ日: 2026/7/4 | 作成者: @davidstosik
- 概要 (1-2文で)
Action Mailer のconfig.action_mailer.delivery_methodのドキュメントを拡充し、循環参照的で分かりにくかった記述を、実際に使えるレベルの説明に整理した PR です。あわせてsmtp_settings/sendmail_settings/file_settingsとの依存関係が分かるように、記述の順番も入れ替えています。
- 変更内容の詳細
※コード変更ではなく、guides/source/configuring.md(Rails Guides の設定ガイド)内の Action Mailer セクションの文章修正です。
実際の差分は概ね以下のような内容を含むと考えてよいです(擬似的な例を含みます):
2.1 delivery_method の説明を具体化
以前のドキュメントでは、delivery_method の説明が他の箇所を参照するだけで、実際に何をどう設定できるのかが分かりにくく、参照が行ったり来たりする「循環」状態でした。
この PR では、以下のような情報が一か所にまとまるように整理されています:
delivery_methodに指定できる主なシンボル例::smtp:sendmail:file:test- (Action Mailer アダプタを追加していれば)独自の delivery method クラス
- それぞれが「どのような形でメールを送る/保存する」のかという概要
:smtp→ SMTP サーバ経由で送信:sendmail→sendmailコマンド経由で送信:file→ 実際には送信せず、メール内容をファイルに書き出す:test→ActionMailer::Base.deliveries配列に溜めるだけ(テスト用)
たとえば、設定例として以下のようなコードがドキュメント内で示されているはずです:
# config/environments/production.rb
Rails.application.configure do
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.example.com",
port: 587,
user_name: ENV["SMTP_USERNAME"],
password: ENV["SMTP_PASSWORD"],
authentication: :login,
enable_starttls_auto: true
}
end2.2 設定項目の順序を依存関係に沿って並べ替え
元のガイドでは、smtp_settings / sendmail_settings / file_settings が先に出てきて、その後に delivery_method が説明されていた、あるいは相互に行き来する形だったため、読み始めの人には依存関係が分かりにくくなっていました。
この PR では:
- まず
config.action_mailer.delivery_methodで「どの配信方法を使うか」を説明 - その後に、選択した配信方法ごとの設定 (
smtp_settings,sendmail_settings,file_settingsなど) を説明
という順番に変えています。
これにより、「delivery_method で :smtp を選んだ場合に、次に smtp_settings をこう書く」という流れが自然に理解できるようになります。
2.3 delivery_method と *_settings の関係を明記
config.action_mailer.smtp_settings などが「どの delivery method を選んだときに有効になるか」が明示されるように説明が補足されています。
例:
config.action_mailer.smtp_settingsはdelivery_method = :smtpのときに意味を持つconfig.action_mailer.sendmail_settingsはdelivery_method = :sendmailのときに使用されるconfig.action_mailer.file_settingsはdelivery_method = :file向け
この関係が文章としてはっきり書かれたことで、「設定を書いたのに効かない」という混乱を防げるようになっています。
- 影響範囲・注意点
- 影響範囲はドキュメント(Rails Guides)のみであり、Action Mailer の動作や API には変更はありません。
- アプリケーション側のコードを書き換える必要はありませんが、今後
delivery_methodを設定する際に、- どの method を選ぶべきか
- どの *_settings が効くか といった点が、今回のガイド更新を読むことで理解しやすくなります。
- 既存プロジェクトで「メールが飛ばない」「設定が効いていない」などのトラブルがある場合、まずこの更新後のガイドを確認すると原因把握に役立ちます。
- 参考情報 (あれば)
- 対応している Issue: #57876
→delivery_methodのドキュメントが循環参照的で分かりにくいという報告への対応。 - 具体的な差分は Rails 本体リポジトリの
guides/source/configuring.mdの変更箇所で確認できます。 - Action Mailer の基本的な設定全体は Rails Guides の「Configuring Rails Applications」内の Action Mailer セクションにまとまっています。
#57972 Make remove_foreign_key(if_exists: true) reversible
マージ日: 2026/7/3 | 作成者: @55728
- 概要 (1-2文で)
Rails のマイグレーションでremove_foreign_key if_exists: trueを実行した際、その逆操作(ロールバック)として生成されるadd_foreign_keyが誤ってif_exists: trueを含んでしまう問題を修正した PR です。これにより、ロールバック時にif_existsが正しくif_not_existsに変換され、操作がより安全かつ意図通りに動作するようになります。
- 変更内容の詳細
何が問題だったか
ActiveRecord::Migration::CommandRecorder は、マイグレーションのコマンドからロールバック用の逆コマンドを自動生成します。
従来は次のような挙動になっていました:
# forward
remove_foreign_key :orders, :users, if_exists: true
# generated inverse (before this PR)
add_foreign_key :orders, :users, if_exists: trueしかし add_foreign_key は if_exists: オプションをサポートしていません。
そのため:
- 生成されたロールバックコードが不正なオプションを含む
- ロールバックの「冪等性」(同じロールバックを何度実行しても問題にならないこと)が崩れる
という問題がありました。
修正内容
この PR では、invert_remove_foreign_key の処理を修正し、if_exists: true をロールバック側では if_not_exists: true に変換するようにしました。
要するに:
# forward
remove_foreign_key :orders, :users, if_exists: true
# generated inverse (after this PR)
add_foreign_key :orders, :users, if_not_exists: trueとなります。
これにより:
- すでに外部キーが存在しない場合でも
remove_foreign_keyが安全に実行できる (if_exists: true) - ロールバック時に外部キーが既に存在していても
add_foreign_key側でif_not_exists: trueにより二重作成を避けられる
という、前後両方向での冪等性が担保されます。
実装箇所
activerecord/lib/active_record/migration/command_recorder.rbinvert_remove_foreign_key(もしくはそれに相当するロジック)に 1 行追加し、オプション変換ロジックにif_exists→if_not_existsのマッピングを追加
activerecord/test/cases/migration/command_recorder_test.rb- 上記変換が正しく行われることを確認するテストを 5 行追加
実装自体はごく小さい変更ですが、挙動の一貫性と安全性を高める修正です。
- 影響範囲・注意点
対象:
remove_foreign_key ... if_exists: trueをマイグレーションで使用しているプロジェクト- 特に
rails db:rollback/rails db:migrate:downなどで自動生成される逆マイグレーションを信頼しているケース
期待される挙動:
- 既存マイグレーションの「前方」実行 (
db:migrate) の挙動は変わりません。 - 新しい Rails バージョンでロールバックすると、逆操作が
add_foreign_key ... if_not_exists: trueとして扱われ、外部キーがすでに存在している環境でもエラーになりにくくなります。
- 既存マイグレーションの「前方」実行 (
注意点:
add_foreign_keyはif_not_exists:をサポートしていることが前提です。古い Rails から途中バージョンを飛ばしてアップグレードする場合は、if_not_exists対応が入っているか(おおむね 7.1 以降)を確認してください。- もし、自前で
CommandRecorderを拡張している場合は、remove_foreign_key/add_foreign_keyまわりのオプション変換ロジックと競合しないか軽く確認すると安全です。
- 参考情報 (あれば)
- 類似の変換ロジック:
invert_add_foreign_keyでif_not_exists→if_existsに変換しているコミット:8d5c5bd2f5invert_remove_check_constraintでも同様にif_existsとif_not_existsの変換を行っており、それと整合する変更です。
- 該当 PR:
#57970 Make ActiveSupport::ProxyLogger#silence suppress logs
マージ日: 2026/7/3 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::ProxyLogger#silenceが期待どおりログを抑制するように修正された PR です。silenceブロック内で、指定した一時的なログレベル未満のメッセージが内部ロガーへ転送されないようになりました。
- 変更内容の詳細(サンプルコード含む)
背景となる問題
ActiveSupport::ProxyLogger は LoggerSilence を include しているものの、これまで silence を呼んでも実際には何も抑制されていませんでした。
proxy = ActiveSupport::ProxyLogger.new(ActiveSupport::Logger.new($stdout))
proxy.silence do
proxy.info("noise")
end
# 期待される動作(ActiveSupport::Logger と同じ):
# 何も出力されない
# 実際の動作(修正前):
# "noise" が出力されていたRails 本体では、マイグレーション処理などの内部で logger.silence を使って冗長なログを抑制しており、
「標準 Logger インターフェースをほぼサポートする」ことをドキュメントで謳っている ProxyLogger が silence を無視してしまうのは契約違反になる、というのが問題意識です。
この PR の主な修正点
ProxyLogger#silenceが、ブロック内でのログレベルを一時的に引き上げて実際に抑制するようになった。LoggerSilenceの期待する挙動に合わせ、ブロック内では一時的な severity(例:Logger::ERROR)より低いログを出さない。- これにより
ActiveSupport::Loggerと同じ挙動になります。
抑制は「その
ProxyLoggerインスタンスのみに限定」されるようになった。- 同じ「内部ロガー(
ActiveSupport::Loggerなど)」を共有している複数のProxyLoggerがある場合、 片方でsilenceしても、もう片方には影響しないように実装されています。 - これは
ActiveSupport::Logger#silenceの挙動と整合的です(ロガー単位ではなく、プロキシごとにスコープされる)。
- 同じ「内部ロガー(
テストの追加
activesupport/test/proxy_logger_test.rbに、次のような観点のテストが追加されています:silenceブロック内で、閾値より低い severity のログが出力されないこと。- 同じ underlying logger を共有する別の
ProxyLoggerが、片方のsilenceの影響を受けないこと。
※ 実際のコードは 4 行追加・1 行削除と小さい変更ですが、要点は「ProxyLogger 経由でログ出力される前に、自身のサイレンス状態を考慮してフィルタする」ようにしたことです。
- 影響範囲・注意点
影響範囲
ActiveSupport::ProxyLoggerを使い、かつproxy_logger.silence { ... }しているコードに影響します。- これまで「
silenceを呼んでも実際には抑制されていなかった」挙動が修正されるため、ログ出力量が減る 可能性があります。 - ただし、
ProxyLogger自体がまだリリース前のクラスであり、公開バージョンではこの挙動に依存しているアプリは基本的に存在しない想定です。
注意点
同じ underlying logger を共有する他の
ProxyLoggerに対しては、silenceは効きません。
例えば以下のケースでproxy1を silence しても、proxy2は通常どおりログを出します:rubybase_logger = ActiveSupport::Logger.new($stdout) proxy1 = ActiveSupport::ProxyLogger.new(base_logger) proxy2 = ActiveSupport::ProxyLogger.new(base_logger) proxy1.silence do proxy1.info("hidden") # → 抑制される proxy2.info("visible") # → 出力される end複数のプロキシ間で一括してサイレンスしたい場合は、基底ロガー側で制御するか、別途ラッピングが必要です。
ログレベルに依存する処理(例:
debugログだけをフックするミドルウェアなど)を ProxyLogger 経由で行っている場合、silenceブロック内ではその処理も呼ばれなくなる可能性があります。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57970
- 関連クラス:
ActiveSupport::ProxyLoggerActiveSupport::LoggerActiveSupport::LoggerSilence/LoggerSilence
logger.silenceは Rails において、マイグレーションや内部処理の一部で冗長なログを抑制する用途で使用されており、本 PR により ProxyLogger でも同じ期待値で利用できるようになります。
#57888 Make autosave association callbacks Ractor-safe
マージ日: 2026/7/3 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record の autosave な関連(autosave: trueな association)で生成されるコールバックメソッドを、Ractor(並行実行環境)でも安全に動作するように実装を変更した PR です。define_method+ ブロックで作っていたメソッドをclass_eval+ シンボル呼び出しに変えることで、非共有Procをキャプチャしないようにしています。
- 変更内容の詳細
背景・問題点
- autosave な関連(例:
has_many :comments, autosave: true)では、内部的に例えば以下のようなメソッドが自動生成されます:autosave_associated_records_for_comments
- これらは Active Record 内部の
define_non_cyclic_methodというヘルパで定義されており、以前はだいたい次のような形で実装されていました(イメージ):
define_non_cyclic_method(:comments) do |owner|
owner.autosave_associated_records_for_comments
end- 実際には
define_methodとブロックを使ってメソッドが定義されており、そのブロックは Ruby のProcオブジェクトとしてクラスにぶら下がります。 - Ractor(Ruby 3+ の並行実行モデル)では、「共有不可能なオブジェクト(unshareable)」を他の Ractor にそのまま渡せません。ブロックから生成される
Procは通常共有不可能オブジェクトのため、それをキャプチャしたメソッドを非メイン Ractor から呼び出そうとすると制約に引っかかります。 - その結果、非メイン Ractor 内で autosave 関連のコールバックが呼び出されるケースで問題が起きうるため、Ractor-safe な形に書き換えた、というのが PR のモチベーションです。
具体的な変更点
PR 説明によると:
define_non_cyclic_methodimplementation to use class_eval and symbols instead ofdefine_methodand blocks.
つまり:
- 変更前:
define_method+ ブロック(Proc をキャプチャ) - 変更後:
class_eval+ メソッド名(シンボル)経由のディスパッチ
のような形になります。
中でやっていることのイメージを、かなり単純化したサンプルで表現すると:
変更前(イメージ)
def define_non_cyclic_method(name, &callback)
define_method("autosave_associated_records_for_#{name}") do
callback.call(self)
end
endここで &callback は Proc であり、define_method のブロックとしてクラスにぶら下がります。この Proc が Ractor 的には共有できないオブジェクトになりうる。
変更後(イメージ)
def define_non_cyclic_method(name, method_name)
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def autosave_associated_records_for_#{name}
#{method_name} # あるいは send(:#{method_name}) 等
end
RUBY
end- 文字列を
class_evalしてメソッドを定義するため、メソッド本体は単に既存のメソッド名を呼び出すだけになります。 - ここでは共有不可能な
Procをクラスにキャプチャしないため、Ractor 間でも問題になりにくくなります。
実際のコードはもう少し複雑ですが、「ブロックをキャプチャして定義する」から「ソースコード文字列で定義し、実際の処理は別メソッド(シンボル)に委譲する」という方向に変わった、と理解しておくと良いです。
パフォーマンス
付属のベンチマークでは、autosave_associated_records_for_comments を大量に呼び出したときの性能を比較しています:
- before: 約 4,491,233 i/s
- after: 約 4,411,090 i/s
差は誤差範囲内("same-ish")とされており、パフォーマンス退行はほぼ無いとみなされています。
- 影響範囲・注意点
影響範囲
- Active Record の autosave 関連コールバックの生成メカニズム(
define_non_cyclic_method)が変更されています。 - 対象となるのは主に:
has_many :foo, autosave: truehas_one :bar, autosave: true- その他 autosave オプション付きの関連に付随する内部的なコールバックメソッド
- 表向きの API(
autosave: trueの指定方法や、関連オブジェクトの保存時挙動)は変わりません。
注意点 / 開発者視点でのチェックポイント
- Ractor を使っていないアプリに対する互換性
- 既存挙動を壊すような変更は意図されておらず、ベンチマーク上も挙動自体は変わらない前提です。
- ただし、メソッド定義の仕方が
define_methodからclass_evalに変わるため、メタプログラミングやmethod_source系のツールでクラス定義を解析している場合に、挙動が微妙に変わる可能性はあります(メソッドのsource_locationなど)。
- Ractor を利用している / 利用予定のアプリ
- この変更により、autosave な関連を持つモデルをサブ Ractor から扱ったときの安全性が向上します。
- ただし Ractor そのものは他にも多数の制約があるため、「これで Active Record が完全に Ractor-safe になった」という保証ではなく、「autosave association callbacks に関する一つの問題を解消した」に留まると考えるべきです。
- テストについて
- チェックリストでは「Tests are added or updated」が未チェックになっています。
- つまり、この PR 単体では新規テストやテスト更新が入っていない可能性があります。
- 既存テストで十分にカバーされている前提か、あるいは別途 Ractor 関連のテストが追加されるフェーズがあるかもしれません。
- Ractor 環境下の挙動についてはまだテストカバレッジが薄い可能性があるため、Ractor を本格利用する場合は手元での実験・検証を推奨します。
- チェックリストでは「Tests are added or updated」が未チェックになっています。
- 参考情報 (あれば)
- Ractor と shareable / unshareable オブジェクトの制約については、Ruby 本体のドキュメントが参考になります:
- Rails の autosave 機能自体の仕様:
autosave: trueの関連は、親オブジェクトのsave時に関連先も自動的にsave/destroyされる機能で、バリデーションやトランザクションと組み合わせて複雑なオブジェクトグラフの整合性を保つ用途でよく利用されます。
- 本 PR の趣旨:
- 「挙動を変えずに実装を Ractor フレンドリーにする」リファクタリングの一種であり、今後の Ruby 並行実行サポートに向けた地ならしと言えます。
#57927 Make Active Record scope methods Ractor-shareable
マージ日: 2026/7/3 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record のscopeが Ractor(並列実行機構)上で利用できるように、スコープ定義で使われるProcを「Ractor-shareable」な形に変更した PR です。これにより、Ractor 内から AR モデルのスコープを安全に呼び出せるようになります。
- 変更内容の詳細
何をしたか
ActiveRecord::Scoping::Named内のscope定義処理を変更し、スコープ定義に使うProcを Ractor で共有可能なオブジェクト(Ractor-shareable)として扱うようにした。- それに合わせてテスト (
named_scoping_test.rb) を追加し、Ractor 上でもスコープが動作することを確認している。
Ruby 3 以降の Ractor では、スレッド安全性を保つために「共有できるオブジェクト」と「共有できないオブジェクト」が区別されており、通常の Proc やクロージャはしばしば「共有不可」とみなされます。この PR は、Active Record の scope が内部的に利用する Proc を、Ractor をまたいで共有できる形に組み立て直しています。
想定されるコードレベルの振る舞い
普段のスコープ定義:
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
endこの PR 以前:
- 上記の
-> { ... }が作るProcは「Ractor-shareable でない」ケースがあり、
Ractor 内からPost.publishedを呼ぶとエラーになったり、利用に制限があった。
この PR 後:
scope :published, -> { ... }で内部的に生成されるProcが、Ractor で共有可能な形になるように処理される。- たとえば、以下のようなコードが動くことをテストしているはずです(イメージ):
r = Ractor.new do
# Ractor 内から Active Record のスコープを呼び出す
Post.published.to_a
end
r.takeテストファイル (named_scoping_test.rb) では、
- スコープが Ractor 内で問題なく呼び出せること
- スコープチェーンや既存の機能との互換性
といった点を確認するケースが追加されています。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveRecord::Base.scopeで定義された全てのスコープ - 利用場面:
- Ractor を使った並列処理の中で Active Record のスコープを呼ぶ場合
- Ractor をまたいでモデルクラス自体を共有するような構成
既存コードのインターフェース(scope :name, -> { ... } の書き方や返り値など)は変わりません。
スコープに関係するのは主に「内部で使われる Proc の性質」なので、通常のアプリケーションで Ractor を使っていなければ、挙動は従来とほぼ同じです。
注意点
Ractor 非対応なオブジェクトをキャプチャするスコープ
スコープ定義内で、Ractor 非共有オブジェクト(ミューテーブルなオブジェクト、スレッドローカルな状態、IO など)をキャプチャしている場合、それが Ractor-shareable なProcとして扱えるかどうかは Ruby 本体/Active Record 側の実装に依存します。
原則として:- スコープには「Ractor 間で共有できる情報」だけを使う
- 外部の状態(特にミューテーブルなオブジェクト)を閉じ込めない
という形にしておくと安全です。
CHANGELOG 未更新
CHANGELOG はまだ更新されていませんが、挙動としては「新機能(Ractor 対応強化)」に近い変更です。将来的にドキュメントや変更履歴に追記される可能性があります。Ruby / Rails のバージョン依存
この PR は Ractor を前提としているため、Ruby 3 以降が前提です。Rails 側がサポートする Ruby バージョンとの組み合わせに依存するので、利用する際は公式のサポートマトリクスを確認してください。
- 参考情報 (あれば)
- Ractor 概要(Ruby公式ドキュメント)
- Active Record Scopes ガイド
- 類似の Ractor 対応 PR(Ruby / Rails 並列実行まわり)を追うと、今後のマルチRactor対応の方向性を把握しやすくなります。
#57961 Add ActiveSupport::Ractors.try_make_shareable
マージ日: 2026/7/3 | 作成者: @gmcgibbon
- 概要 (1-2文で)
ActiveSupport::Ractors.try_make_shareableという新しいメソッドが追加され、任意のオブジェクトを「Ractor 共有可能(shareable)」にしようと試みる共通ユーティリティが ActiveSupport に入りました。unshareable_proc_actionの設定に応じて、共有不可な Proc などにぶつかったときの挙動(例外・警告・無視)を切り替えられます。
- 変更内容の詳細
追加された API
activesupport/lib/active_support/ractors.rb に以下のようなメソッドが追加されています(概念的なイメージ):
module ActiveSupport
module Ractors
class << self
# すでに存在するもの (イメージ)
# def try_shareable_proc(proc, unshareable_proc_action: ...)
# ...
# end
# 今回追加されたもの
def try_make_shareable(object, unshareable_proc_action: Rails.application.config.active_support.unshareable_proc_action)
case unshareable_proc_action
when :raise
::Ractor.make_shareable(object) # Ractor::IsolationError がそのまま飛ぶ
when :warn
begin
::Ractor.make_shareable(object)
rescue Ractor::IsolationError => error
ActiveSupport::Deprecation.warn(<<~MSG)
Ractor isolation error while trying to make object shareable: #{error.message}
MSG
object # エラーは握りつぶして元のオブジェクトを返す/処理を続行
end
when nil
begin
::Ractor.make_shareable(object)
rescue Ractor::IsolationError
object # 完全に無視(ログ・警告も出さない)
end
else
# 想定外の設定値が来た場合のフォールバック(多くは :raise と等価)
::Ractor.make_shareable(object)
end
end
end
end
end※上記は PR 説明とテストから読み取れる挙動イメージです。実際のコードとはメソッド名や細部が多少異なる可能性があります。
Rails にはすでに「Proc 専用」の try_shareable_proc が存在しており、この PR は「任意オブジェクト版」の try_make_shareable を追加したものです。#57852 など他の PR/機能から、この共通メソッドを呼び出す前提になっています。
unshareable_proc_action による挙動の違い
unshareable_proc_action は ActiveSupport の既存設定で、Proc が Ractor 的に共有できない場合どう扱うかを決めるフラグです。この PR では**「オブジェクト全般を Ractor 共有化しようとしたときに発生する isolation error」に対しても、その設定を流用する**ようになっています。
:raise(デフォルト想定)Ractor.make_shareable(object)実行時にRactor::IsolationErrorが起これば、そのまま例外として呼び出し元に飛ぶ。- Ractor 安全性が最も厳密。
:warnRactor::IsolationError発生時に、ActiveSupport の警告(Deprecation.warnなど)としてログを出し、処理自体は継続。- 実際にはオブジェクトは共有可能にならない(/なれなかった)可能性があるが、アプリは止めずに挙動を観測できる。
nilRactor::IsolationErrorを完全に握りつぶし、何も出さない。- Ractor 共有化に失敗しても無視して続行したい場合のため。
テスト
activesupport/test/ractors_test.rb に 50 行超のテストが追加され、主に以下をカバーしていると考えられます:
try_make_shareableが:raise,:warn,nilの各設定値で期待通りの挙動をすること- 共有可能なオブジェクト(例: freeze 済みの単純な Hash/Array など)が問題なく共有可能になること
- 共有不可能なオブジェクト(Proc を含むオブジェクト)が、設定に応じて例外・警告・無視として扱われること
- 影響範囲・注意点
Ractor を利用している / 利用予定の Rails アプリ・ライブラリ
Ractor.make_shareableを直接呼ぶ代わりに、ActiveSupport::Ractors.try_make_shareableを使うことで、環境設定に応じてエラー処理を一元化できます。- Ractor 対応のコードを書くとき、例外をどう扱うかを都度実装する必要が減ります。
unshareable_proc_action設定の意味合いが広がる- これまでは「Proc を共有可能にするときの挙動」を制御する設定でしたが、この PR によって「任意オブジェクトを Ractor 共有可能にしようとするときの挙動」にも影響します。
- 既に
unshareable_proc_actionをカスタマイズしているアプリでは、オブジェクト共有化まわりの挙動にもそのポリシーが適用される点を意識する必要があります。
例外を握りつぶす設定に注意
:warnやnilを使うと、「本来は Ractor 共有されている前提でコードを書いたが、実際には共有化されていない」という状態になりうるため、マルチ Ractor 実行時のデータ競合や設計ミスに気付きにくくなります。- 本番運用ではまず
:raiseで問題の有無を洗い出し、問題箇所を修正した上で必要に応じて:warn/nilを検討するのが安全です。
- 参考情報 (あれば)
既存の Proc 用 API:
try_shareable_procを追加した PR: https://github.com/rails/rails/pull/57626本 PR で追加された API を利用予定の PR:
Ruby 公式ドキュメント(Ractor):
- https://docs.ruby-lang.org/ja/latest/class/Ractor.html
- 特に
Ractor.make_shareableとRactor::IsolationError周りが今回の変更と関連します。
#57976 Make ActiveSupport::EventReport::LogSubscriber ractor safe
マージ日: 2026/7/3 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::EventReport::LogSubscriberのevent_log_levelAPI が Ractor セーフになるように、内部で使っているlog_levelsの扱いを「コピー&追記&freeze」に変更した PRです。共有ハッシュをそのまま書き換えないことで、Ractor 間でのスレッドセーフ/Ractor セーフな利用ができるようになっています。
- 変更内容の詳細(あればサンプルコードも含めて)
※実際の差分は2行だけですが、説明のために典型的な変更前後イメージを示します(正確なメソッド名や変数名は若干異なる可能性があります)。
変更のポイント
log_levels(イベント名 => ログレベル のマッピング)を破壊的に更新しないようにした。- 新しいイベントログレベルを追加するときに
- 既存のハッシュを
dup - その dup したハッシュに新しいキー/値を追加
- そのハッシュを
freeze - それを元の参照に再代入
というフローに変更。
- 既存のハッシュを
このパターンは「イミュータブルなハッシュをバージョンアップしていく」やり方で、Ractor から見ても「freeze 済みのオブジェクト」を共有するだけになるため安全です。
イメージコード
変更前(イメージ):
def self.event_log_level(event_name, level)
log_levels[event_name.to_sym] = level
end変更後(イメージ):
def self.event_log_level(event_name, level)
new_log_levels = log_levels.dup
new_log_levels[event_name.to_sym] = level
@log_levels = new_log_levels.freeze
endここでのポイント:
- すでに
@log_levels自体がfreezeされている想定で、その frozen なハッシュを直接書き換えなくなった。 - 毎回 dup してから freeze することで、「書き換え可能なオブジェクト」が Ractor 間で共有されなくなる。
テストの追加
activesupport/test/event_reporter/log_subscriber_test.rb に6行追加されており、主に以下の点を検証していると考えられます:
event_log_levelを複数回呼んでも例外が出ないことlog_levelsが常にfreezeされていること- (Ruby が Ractor をサポートする環境であれば)Ractor 内から
event_log_levelを使ってもエラーにならないこと、など
- 影響範囲・注意点
- 対象:
ActiveSupport::EventReport::LogSubscriberを直接または経由して利用しているコード。- 主に「イベントレポートのログレベルを動的にカスタマイズしている」ようなコードが影響範囲です。
- 挙動面:
- 公開APIとしては
event_log_levelの使い方・結果は変わらない想定です(同じイベントに対するログレベルが期待通り設定される)。 - 内部実装のみがスレッド/Ractor セーフな書き方に変わっています。
- 公開APIとしては
- パフォーマンス:
event_log_level呼び出し毎にlog_levelsハッシュのdupとfreezeが入るため、わずかにオーバーヘッドは増えます。- ただし、この API は「起動時設定や初期化フェーズで数回呼ぶ」程度のことが多く、通常のリクエストパス上で高頻度に呼ぶことは想定されていないため、実務上は問題にならないケースがほとんどです。
- マルチスレッド/Ractor:
- 複数スレッドや Ractor から
event_log_levelを呼び出しても、共有の frozen ハッシュを直接書き換えないため、競合や Ractor 不正アクセスエラーを避けられます。 - 既に Ractor を使っているアプリケーションでは、この変更により「イベントログレベル設定」に起因するエラーが起きにくくなります。
- 複数スレッドや Ractor から
- 参考情報 (あれば)
- Ractor セーフにする際の典型パターン:
- 共有データ構造は
freezeして不変にする。 - 更新が必要な場合は「
dup→ 変更 →freeze→ 参照を差し替え」のパターンを取る。
- 共有データ構造は
- Rails / ActiveSupport における類似の対応:
- キャッシュ設定やログ設定など、グローバルなコンフィグを扱う箇所では、同様の「イミュータブルなハッシュ/配列を差し替える」手法が徐々に導入されています。
- 本 PR は CHANGELOG 更新は行われておらず、「外部API仕様は変えずに内部を Ractor フレンドリーにした小規模バグ修正/改善」という位置づけです。
#57978 Make Normalization.normalizes ractor safe
マージ日: 2026/7/3 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveModel::Normalization.normalizesが Ractor(マルチスレッド並列実行)環境でも安全に利用できるよう、正規化属性を追加する内部実装を「複製してから追記して凍結する」形に変更した PR です。挙動自体は変えずに、内部データ構造の扱い方だけをスレッド/Ractor安全寄りにしています。
- 変更内容の詳細
何をやったか
ActiveModel::Attributes::Normalization モジュール内で、normalizes 呼び出し時に使う内部配列(もしくは類似のコレクション)の扱いを以下のように変更しています。
- これまで: 既存の配列に対してそのまま
<<などで追記(破壊的変更) - 変更後:
- 既存の配列を
dupで複製 - 複製した配列に追記 (append)
- その配列を
freezeして不変オブジェクトとして保持
- 既存の配列を
これにより、ある Ractor で参照中の配列が、別の Ractor から破壊的に変更されることを防いでいます。
PR本文で書かれていた「Dup, append and freeze.」がまさにこのパターンです。
ざっくりしたイメージコード
実際のコードとは多少違う可能性がありますが、意図としては以下のような変化です:
# 変更前のイメージ
def normalizes(name, **options, &block)
@normalizations ||= []
@normalizations << build_normalization(name, options, &block)
end
# 変更後のイメージ
def normalizes(name, **options, &block)
@normalizations ||= [].freeze
new_normalizations = @normalizations.dup
new_normalizations << build_normalization(name, options, &block)
@normalizations = new_normalizations.freeze
endポイント:
- 共有されるインスタンス変数(例:
@normalizations)自体は置き換え(再代入)で更新し、
「すでに公開済みのオブジェクト」は一切破壊的に書き換えない。 - 新しく作る配列は最終的に
freezeされるため、Ractor 間で共有しても書き換え不可能。
テストの追加
activemodel/test/cases/attributes/normalization_test.rbに 7 行追加されています。- 目的は主に:
normalizes呼び出し後も内部状態が適切であること- 複数回
normalizesを呼んだときに前の定義が壊れないこと - 凍結配列を前提にした挙動でも問題ないこと
などを確認するものと考えられます(Ractor そのものを直接テストしているかはコード次第ですが、「破壊的変更しない実装」になっていることを保証するテストが入っているはずです)。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveModel::Attributes::Normalizationを利用しているコード、具体的には:
class User
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Attributes::Normalization
attribute :email, :string
normalizes :email, with: ->(value) { value.to_s.strip.downcase }
endのように normalizes を使っている箇所。
- 表面的な挙動(値がどう正規化されるか)は変わりません。
- 主に内部の属性正規化情報テーブルの更新手順のみが、破壊的更新から「イミュータブルな再割り当て」方式に切り替わっています。
Ractor 安全性について
- この PR によって:
- 「normalized 属性を追加する処理」自体が Ractor セーフに近づいた
(既存オブジェクトの破壊的変更を避けるため)
- 「normalized 属性を追加する処理」自体が Ractor セーフに近づいた
- ただし PR 中でも明記されている通り:
Note, attribute registration that this relies on is still not ractor safe, its slightly more involved. Someone will do it next week probably.
- つまり:
- 「属性(
attribute)自体の登録」に関わるコードは、まだ Ractor セーフではない。 normalizes周辺だけが先行して Ractor セーフなスタイルにリファクタされている状態。
- 「属性(
- 結論として:
- この PR だけで ActiveModel 属性周り全体が完全に Ractor セーフになるわけではない
- とはいえ、将来的な完全 Ractor 対応に向けた一部ステップであり、
少なくともnormalizesによる内部配列の共有・書き換えに起因する Ractor 競合バグは出にくくなります。
パフォーマンス・互換性
dup+freezeにより:normalizes呼び出しごとに配列のコピーが発生するため、属性数・normalizes呼び出し回数が非常に多い場合には、わずかなオーバーヘッド増加があり得ます。- ただし
normalizes自体は通常アプリケーション起動時にクラス定義の中で呼ぶことが多く、リクエストごとに大量に呼ばれるものではないため、実務上は問題になりにくいと考えられます。
- API 互換性:
- 公開 API (
normalizesの引数や動作) は変わっていないため、既存コードは基本的にそのまま動作します。
- 公開 API (
- 参考情報 (あれば)
Ruby Ractor とイミュータブルデータ構造:
- Ractor 間で共有できるのは、基本的に「凍結されたオブジェクト」や Ractor セーフな型だけであり、可変オブジェクトを共有し破壊的変更するとエラーや未定義動作の原因になります。
- この PR の
dup→append→freezeパターンは、Ractor 対応で頻出する「内部状態をイミュータブルにする」ための典型的リファクタです。
今後の想定:
- PR 本文にある通り、属性登録周りの Ractor 対応が別 PR で行われる予定で、
それと組み合わさることで ActiveModel 属性システム全体がより Ractor フレンドリーになっていきます。
- PR 本文にある通り、属性登録周りの Ractor 対応が別 PR で行われる予定で、
#57979 Use latest ruby for GHA integration tests
マージ日: 2026/7/3 | 作成者: @skipkayhil
概要 (1-2文で)
このPRは、GitHub Actions 上で動作する Rails の各種インテグレーションテスト(devcontainer、rails new docker、rail_inspector)で使用する Ruby のバージョンを「最新の Ruby」に切り替える変更です。テストフロー自体は変えず、実行環境としての Ruby の指定のみを更新しています。変更内容の詳細(あればサンプルコードも含めて)
対象ファイルはいずれも GitHub Actions のワークフローファイルです。
.github/workflows/devcontainer-smoke-test.yml.github/workflows/rail_inspector.yml.github/workflows/rails-new-docker.yml
各ワークフロー内で、Ruby のバージョンを指定している箇所が 1 行ずつ書き換えられています。具体的には、従来は固定バージョン(例: 3.3, 3.2 など)を指定していたものを、「最新の安定版 Ruby」を指す指定に変更した形が想定されます。
典型的には、以下のような変更になっている可能性が高いです(参考イメージ):
# 変更前(例)
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
# 変更後(例)
- uses: ruby/setup-ruby@v1
with:
ruby-version: 'ruby' # または '3.4' など、その時点の最新もしくは、matrix で定義している ruby: [ '3.3' ] を最新バージョンに上げる、という形の可能性もあります。
いずれにせよ、各ワークフローの「使う Ruby のバージョンを 1 行だけ差し替えている」以外の変更は行われていません。
- 影響範囲・注意点
影響範囲
- GitHub Actions 上で実行される以下のワークフローの Ruby 実行環境:
- devcontainer-smoke-test
- rail_inspector
- rails-new-docker
- これらが「常に最新の Ruby で Rails をテストする」状態になるため、Rails 本体が最新 Ruby に対して継続的に検証されるようになります。
- GitHub Actions 上で実行される以下のワークフローの Ruby 実行環境:
メリット
- Ruby の新バージョンがリリースされた際に、自動的にそのバージョンで検証が走るようになり、Ruby 側の変更による非互換や警告の早期検知が可能になります。
- Rails の「最新 Ruby サポート状況」が CI 上で常にカバーされるため、メンテナンス性の向上に寄与します。
注意点 / リスク
- Ruby の新バージョンで非互換やバグが出た場合、Ruby リリース直後に CI が急に壊れる可能性があります(=Rails 側が即座には対応できていないケース)。
- もし「特定の Ruby バージョンでの動作保証」を厳密にテストしたい場合は、別のジョブやマトリクスで LTS 的なバージョン(例:
3.1,3.2など)を並行して運用する必要があります。 - 新しい Ruby でのみ発生する警告(deprecation など)が増え、ログがノイジーになる可能性があります。
- 参考情報 (あれば)
ruby/setup-rubyアクションのバージョン指定方法(ruby-version: rubyで「最新安定版」を指す等)
https://github.com/ruby/setup-ruby#usage- Rails がサポートしている Ruby バージョンポリシー(ガイドライン)
https://guides.rubyonrails.org/ (「Ruby on Rails Guides」内の “Getting Started” や “Maintenance Policy” 関連) - Rails リポジトリ内の GitHub Actions 設定全体
https://github.com/rails/rails/tree/main/.github/workflows
#57974 Require useragent when loading ActionController::AllowBrowser
マージ日: 2026/7/3 | 作成者: @andrewn617
- 概要 (1-2文で)
ActionController::AllowBrowserを読み込むタイミングでuseragentをrequireするように変更し、これまで「最初のリクエスト時」に行われていたuseragentの読み込みを事前に行えるようにした PR です。主に eager load 環境や Ractor 対応時の初回読み込みタイミングの改善が目的です。
- 変更内容の詳細
※PR本文からはファイル単位の情報しかありませんが、内容的には以下のような極小変更です(+2/-2 行)。
対象ファイル:
actionpack/lib/action_controller/metal/allow_browser.rb
やっていることは要約すると:
- これまで:
AllowBrowserが使われるタイミング、あるいは最初のリクエスト処理のどこかでuseragentをrequireしていた(=eager load していても、実際の最初のリクエスト時に初回 require が走る)。
- これから:
ActionController::AllowBrowserクラス/モジュールがロードされる段階でrequire "useragent"を実行するように変更。
イメージとしては、以下のような差分になっている可能性が高いです(擬似コード):
# 変更前 (例)
module ActionController
class AllowBrowser
# どこかのメソッド内や遅延ロード処理の中で:
# require "useragent"
end
end
# 変更後 (例)
require "useragent" # ← ファイルロード時に実行
module ActionController
class AllowBrowser
# ここでは useragent はすでにロード済み
end
endこれにより、AllowBrowser を使う環境では、そのファイルがロードされた時点で useragent もロードされます。
- 影響範囲・注意点
- 影響範囲
ActionController::AllowBrowserを使用しているアプリ・ライブラリ。- 特に eager load を有効にしている本番環境や、Ractor を利用するような並行実行環境での挙動に影響します。
- 具体的な挙動の変化
- 以前: 最初のリクエスト処理中に
useragentのrequireが発生し、そのタイミングでロードコストやスレッド/Ractor との競合リスクがあった。 - 以後: Rails が
ActionController::AllowBrowserをロードした時点でuseragentが確定的にロードされるため、最初のリクエスト時のオーバーヘッドが減り、Ractor 的にも安全になりやすい。
- 以前: 最初のリクエスト処理中に
- パフォーマンス・メモリ
AllowBrowserを利用している環境では、「どうせいつかはuseragentを読む」ので、初回リクエスト前に読むか後に読むかの違いであり、全体的なメモリ使用増はほぼ変わりません。- ただし、
AllowBrowserを定義だけ読み込んで実際には一切使わないような特殊な構成では、以前よりも早いタイミングでuseragentがロードされる可能性があります。
- 互換性
useragent自体の API 変更はなく、あくまで require のタイミング変更のため、一般的なアプリでは後方互換性上の問題はほぼありません。useragentgem を明示的に Gemfile に入れていない状況で(たまたま別の依存関係から入っていたなど)、ロード順に依存していたような非常にレアなケースでは、LoadErrorの発生タイミングが変わる可能性があります。
- 参考情報 (あれば)
- Ractor との関連:
- Ractor 環境では、スレッドセーフでない初回 require や定数定義が、並行実行と絡んで問題を起こし得るため、「最初のリクエスト中に require される」ようなパターンは避けたい。
- 本PRにより、
AllowBrowserのロードタイミングでuseragentが確定的に読み込まれるため、Ractor 開始前に eager load を済ませる設計と相性が良くなります。
- 実運用での確認ポイント:
AllowBrowserを利用しているアプリでは、本番起動時(Rails boot 時)にuseragentが読み込まれることを前提に、必要ならば Gemfile にgem "useragent"が明示的に含まれているかを確認しておくと安全です。
#57977 Fix typographical errors from CHANGELOG files[ci skip]
マージ日: 2026/7/3 | 作成者: @nisusam
概要 (1-2文で)
このPRは、Rails の CHANGELOG ファイル内に含まれていた英単語のタイポ(綴り誤り)を修正するものです。コードや挙動は一切変更せず、ドキュメントの品質向上のみを目的としています。変更内容の詳細(あればサンプルコードも含めて)
対象ファイル
actionpack/CHANGELOG.mdactionview/CHANGELOG.mdrailties/CHANGELOG.md
変更内容
- 各 CHANGELOG に記載されている文章中の typographical error(スペルミスや細かな表記揺れ)を、それぞれ正しい綴りに修正しています。
- 追加行数 3 / 削除行数 3 から分かる通り、修正は行単位で 1 箇所ずつの軽微な置き換えであり、文面の意味や履歴内容自体は変えていません。
- 例としては以下のような変更が想定されます(イメージであり、実際の差分そのものではありません):diff
- * Fixes a behvaior where ... + * Fixes a behavior where ... - * Depreacted option ... + * Deprecated option ...
Ruby コード・設定ファイル・テストコードなど、実行時に読み込まれる部分への変更は行われていません。
- 影響範囲・注意点
影響範囲
- 影響するのはドキュメント(CHANGELOG)のみであり、Rails アプリケーションの実行時挙動、ビルド、テスト、API は一切影響を受けません。
- 既存のバージョン互換性・マイグレーション・設定などにも影響なし。
注意点
- CI スキップ指定(
[ci skip])がタイトルに入っており、テストが走っていない点はログ上の事実としてありますが、そもそも実行可能コードに触れていないため問題にはなりません。 - CHANGELOG の参照時に、以前のタイポに依存したリンクや検索(例: タイポした単語で全文検索していた)を行っていた場合は、今後は正しい綴りで検索する必要があります。
- CI スキップ指定(
- 参考情報 (あれば)
- PR タイトルから分かる通り、この変更は「CHANGELOG ファイル中の誤字修正」に限定されており、Rails の機能追加やバグ修正とは無関係です。
- Rails では、コードと直接関係ないドキュメント変更についても PR ベースで管理し、CI をスキップして軽量にマージする運用が行われています。
#57959 Fix polymorphic belongs_to primary key for sharded targets
マージ日: 2026/7/3 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
- シャーディング対応で「複合主キーを持つ has_many」と「単一外部キーを持つ polymorphic belongs_to」を組み合わせたときに、
AutosaveAssociation内で主キーと外部キーの対応付けがずれてActiveModel::MissingAttributeErrorが起きる回 regress を修正した PRです。 BelongsToReflection#association_primary_keyが、belongs_to 側が単一外部キーしか持たない場合でも、シャーディング用の複合主キー(query_constraints)をそのまま返してしまっていた挙動を是正しています。
- 変更内容の詳細
問題の背景
- シャーディング対象モデルは、典型的に複合主キーのような「問い合わせ上の主キー」を持ちます:
- 例:
[:shard_id, :id]をquery_constraintsとして扱う
- 例:
- 一方で、対応する
belongs_to側は単一の外部キーしか持たないケースがあります:- 例:
belongs_to :shipment, foreign_key: :shipment_id, polymorphic: trueのような形
- 例:
- Rails 7.2 系での refactor により、
AutosaveAssociation内で使われていたcompute_primary_keyが削除され、代わりにBelongsToReflection#association_primary_keyの戻り値がそのまま使われるようになりました(該当コミット)。 AutosaveAssociationは「関連先の primary_key」と「自分の foreign_key」をArray#zipして対応付ける実装になっているため、- primary_key 側が
[:shard_id, :id](長さ2) - foreign_key 側が
[:shipment_id](長さ1) のように長さが合わないと、 shard_idの対応 foreign_key が存在せずnil- それを読み出そうとしたときに
ActiveModel::MissingAttributeErrorが発生してしまう、という不具合が生じていました。
- primary_key 側が
原因になっていた association_primary_key の挙動
BelongsToReflection#association_primary_key は、関連の解決のために使う「関連先の主キー的なカラム」を求めるメソッドです。今回問題になったのは2つの分岐です:
inverse_ofがある場合の分岐:inverse.options[:primary_key]をそのまま採用していた- 通常の「単一カラムの custom primary key(例:
:uuid)」なら正しいが、 - シャーディング対応で
has_many側にprimary_key: [:shard_id, :id]などの「複合 primary_key もどき」が指定されている場合、belongs_to側は単一外部キーでも composite をそのまま採用してしまう
- 上記以外のフォールスルー分岐:
- ターゲットモデルが
has_query_constraints?の場合、query_constraints(複合主キー的な配列)を展開して返す - その際、
belongs_toが「明示的に単一foreign_keyを指定している」ことを考慮していなかった
- ターゲットモデルが
その結果、belongs_to 側が単一 foreign_key にもかかわらず、関連先の「複合 primary key」配列が返ってきてしまい、AutosaveAssociation 側の zip で長さが合わずに例外を起こしていました。
修正内容のポイント
今回のコミットは、過去に compute_primary_key で行っていた「2つのガード」を association_primary_key に復元する形で問題を解消しています。
inverse_ofブランチのガード復活- 目的:
inverse.options[:primary_key]が「複合キー由来」のときに採用しないようにする - 実装の考え方:
has_many側が「複合 foreign_key」を宣言しているとき、その情報は:query_constraintsにも反映される- したがって、
inverse.optionsに:query_constraintsが存在する場合、そこからは「複合キー前提」であることが分かる
- 修正:
association_primary_key内でinverse.options[:query_constraints]の有無を確認し、- ある場合: composite な inverse primary_key(≒query_constraints 由来)として扱うため、
inverse.options[:primary_key]をそのまま採用しない - ない場合: 本当に単一の custom primary_key(例:
uuid)を指定していると判断し、inverse.options[:primary_key]を採用する
- ある場合: composite な inverse primary_key(≒query_constraints 由来)として扱うため、
- 目的:
フォールスルーブランチのガード復活
- 目的:
belongs_to側が明示的に単一foreign_keyを指定しているときには、関連先も単一 primary_key に解決するようにする - 修正:
has_query_constraints?なターゲットであっても、belongs_toで「単一のforeign_keyが明示されている」場合は、query_constraintsを複合キーとしてそのまま返さず、- 単一の primary_key(
primary_keyないし id)に解決するようなロジックを追加
- これにより、
zipの対象が- primary_key 側:
[:id](または同等) - foreign_key 側:
[:shipment_id]となり、要素数が一致して安定して動作する
- primary_key 側:
- 目的:
テストの追加・変更
activerecord/test/cases/associations/belongs_to_associations_test.rb- polymorphic belongs_to + シャーディング対象モデル + inverse_of が絡むケースをカバーするテストを追加
activerecord/test/cases/autosave_association_test.rbAutosaveAssociation経由で保存されるシナリオ(accepts_nested_attributes_for等)の中で、例外が発生しないことを確認する回帰テストを追加
activerecord/test/models/adjustment.rb,shipment.rb,schema.rb- 上記テスト用のモデル・スキーマ定義を拡張
- シャーディング風の
query_constraints/複合 primary_key もどき + polymorphic belongs_to の組み合わせを表現するためのフィクスチャモデルを追加
- 影響範囲・注意点
- 影響を受ける可能性が高いケース:
- シャーディングやマルチテナント構成で
- モデルに
query_constraints(例:[:shard_id, :id]等)を設定している - そのモデルに対して polymorphic な
belongs_toを張っている - inverse 側に
has_many+primary_key: [:shard_id, :id]のような宣言をしている - かつ
AutosaveAssociation経由(accepts_nested_attributes_forなど)で保存している
- モデルに
- シャーディングやマルチテナント構成で
- そのようなアプリでは、Rails のバージョン更新後に以下のような症状が出ていた場合、この PR によって解消される可能性があります:
ActiveModel::MissingAttributeErrorがautosaveのタイミングで突然出る- メッセージの原因が「関連先モデルの
shard_idなど、複合キーの一部が nil とみなされている」ように見える
- 互換性の観点:
- 通常の(非シャーディング・非複合キー)
belongs_to / has_manyには影響しない設計になっています。 - 単一カラムの custom primary_key(例:
primary_key: :uuid)を使っている場合も、inverse.options[:query_constraints]が無い限りはこれまでどおりinverse.options[:primary_key]が採用されるため、挙動は維持されます。
- 通常の(非シャーディング・非複合キー)
- 注意点:
- アプリ側で「belongs_to は単一外部キーだが、関連先は複合 query_constraints を前提にしている」という、かなりトリッキーなカスタマイズをしていた場合、今回の「単一 foreign_key 優先の解決ロジック」により挙動が変わる可能性があります。
- 特に、独自に
query_constraintsをオーバーライドしているような高度なシャーディング実装を行っている場合は、この PR を含む Rails へのアップデート時に関連の保存・ロードの挙動をテストで確認することを推奨します。
- 参考情報 (あれば)
- 回帰のきっかけとなったコミット:
compute_primary_keyがAutosaveAssociationから削除された変更- 7b429b51183566dba3286645323eaf48a88220b0
- 関連クラス・メソッド:
ActiveRecord::Reflection::BelongsToReflection#association_primary_keyActiveRecord::AutosaveAssociationActiveRecord::Reflection#has_query_constraints?とquery_constraints
- 実際のコードを確認したい場合は、
activerecord/lib/active_record/reflection.rbのassociation_primary_key実装と、autosave_association_test.rbに追加されたテストケースを読むと、
「inverse に複合 primary_key がある / belongs_to に単一 foreign_key がある」という具体的な差分が把握しやすくなります。
#57942 Return all parameters from deconstruct_keys when no keys are requested
マージ日: 2026/7/3 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parametersに対するパターンマッチで、{ key:, **rest }のように 特定キーを指定せず rest バインディングだけを使った場合にマッチしなかった問題を修正し、Hash#deconstruct_keysと同じ挙動(キー指定なしなら全パラメータを返す)になるようにした変更です。
- 変更内容の詳細
これまでの問題点
Ruby 3 以降のパターンマッチ(case ... in)は、ハッシュライクなオブジェクトに対して deconstruct_keys を呼び出してパターンにマッチさせます。
ActionController::Parameters はこのインターフェイスを実装していますが、
パターン側で特定のキーをひとつも要求していない場合(in { **rest } や、キーは指定するけど rest のみを実質使うようなケース)に、deconstruct_keys が 空ハッシュ {} を返してしまう挙動になっており、結果としてマッチ失敗になっていました。
PR で説明されている再現コード:
params = ActionController::Parameters.new(name: "Bob", age: 22).permit!
case params
in { name:, **rest }
# Before: ここに来ない(マッチしない)
# After: name => "Bob", rest => { age: 22 }
end原因は、deconstruct_keys(nil)(=「特定のキーは要求していない」という意味)に対して、ActionController::Parameters が空ハッシュを返していたためです。
Ruby の Hash#deconstruct_keys は、引数が nil のとき「全キー・全値」を返す仕様なので、ActionController::Parameters だけ挙動が異なり、パターンマッチが期待通り動かない不整合がありました。
今回の修正内容
actionpack/lib/action_controller/metal/strong_parameters.rb の deconstruct_keys 実装が修正されました:
- 引数
keysがnilの場合に
→ 全パラメータを返すように変更 - これにより
Hash#deconstruct_keysと同じ挙動になります。
テスト (actionpack/test/controller/parameters/equality_test.rb) では、おそらく次のような性質が確認されています(実際のコードは要約):
ActionController::Parameters.new(name: "Bob", age: 22).permit!に対し、case params; in { name:, **rest }; ...; endがマッチすることnameが"Bob"になることrestが{ age: 22 }相当になること
動作イメージ(サンプル)
変更後の振る舞い:
params = ActionController::Parameters.new(
name: "Bob",
age: 22,
admin: true,
).permit!
case params
in { name:, **rest }
# deconstruct_keys(nil) が全パラメータを返すのでマッチする
# name => "Bob"
# rest => { "age" => 22, "admin" => true } あるいは Parameters 相当
end
case params
in { **rest }
# これもマッチする(rest にすべてが入る)
end- 影響範囲・注意点
- 影響を受けるコード
ActionController::Parametersを直接case ... inでマッチさせているコード- 特に
{ key:, **rest }や{ **rest }のような rest バインディングを使うパターン を利用している場合に挙動が変わります。
- 以前との違い
- 以前は「マッチしない」ケースが、「マッチして全パラメータが渡される」ようになります。
- つまり、以前は
else側に流れていた分岐が、in { ... }側に入るようになる可能性があります。
- 互換性
- Ruby 標準の
Hashと同じdeconstruct_keys挙動になるため、設計上はむしろ一貫性が向上した変更です。 - ただし、「
ActionController::Parametersではこのパターンはマッチしないはず」という前提で書かれていた奇妙なワークアラウンドがあれば、その前提は崩れます。
- Ruby 標準の
- Strong Parameters / permit の扱い
- 例では
permit!済みのオブジェクトになっており、通常は 許可されたパラメータの集合がdeconstruct_keysの対象です。 - 未許可のパラメータを含む場合、もともとの
ActionController::Parametersの振る舞い(フィルタリングの有無など)に依存するため、permit/requireの位置づけは引き続き意識する必要があります。
- 例では
- 参考情報 (あれば)
- Ruby 本体の仕様:
Hash#deconstruct_keys- 引数が
nilのとき「ハッシュ自身を返す」という仕様で、今回の変更はこれに追従しています。
- 引数が
- Rails側 PR:
Return all parameters from deconstruct_keys when no keys are requested(Rails PR #57942)ActionController::Parametersを Ruby のパターンマッチ機構とより自然に連携させるための小さな互換性改善です。
#57965 8 1 stable cp 1dbad939f8
マージ日: 2026/7/3 | 作成者: @p8
- 概要 (1-2文で)
このPRは、Rails 8.1 ステーブルブランチに対して「product_reviews」という新しいガイドを大きく追加し、既存の「wishlists」ガイドをわずかに調整する、ドキュメント中心の変更です。コード本体には手が入っておらず、開発者向けの学習・サンプルアプリケーション的なガイドが充実した形になります。
- 変更内容の詳細
2-1. guides/source/product_reviews.md の新規追加 (+1350行)
1350行規模の大きなガイドが新規に追加されています。内容からすると、以下のような「Rails 8.1 を用いた Product Reviews(商品レビュー)機能」のチュートリアル/サンプルアプリ的ガイドである可能性が高いです(Wishlists ガイドと同系統のシリーズであることが多いです)。
想定される構成・内容:
アプリケーション概要
- ECサイトや商品カタログに対して「レビュー」や「評価(rating)」を投稿できる機能を題材にしたガイド
- Rails 8.1 の標準的なスタック(Zeitwerk, importmap / jsbundling, turbo, stimulus など)を前提とした作り
モデル設計例
Product/User/Reviewなどの関連付け典型的には下記のような関連を解説していると考えられます:
rubyclass Product < ApplicationRecord has_many :reviews, dependent: :destroy end class Review < ApplicationRecord belongs_to :product belongs_to :user validates :rating, presence: true, inclusion: { in: 1..5 } validates :body, presence: true end class User < ApplicationRecord has_many :reviews, dependent: :nullify end
マイグレーションとスキーマ設計
reviewsテーブルのカラム例:product_id/user_id(外部キー)rating(整数)body(テキスト)created_at,updated_at
- インデックスや外部キー制約の付け方
コントローラとルーティング
resources :products do resources :reviews, only: %i[create update destroy] endのようなネストされたリソースルーティングReviewsControllerでのcreate,update,destroyアクション実装例rubyclass ReviewsController < ApplicationController before_action :set_product def create @review = @product.reviews.build(review_params.merge(user: current_user)) if @review.save redirect_to @product, notice: "Review was successfully created." else render "products/show", status: :unprocessable_entity end end private def set_product @product = Product.find(params[:product_id]) end def review_params params.require(:review).permit(:rating, :body) end end
ビュー・UI 実装の解説
- 商品詳細ページにレビュー一覧と投稿フォームを組み込む例
form_with model: [@product, @product.reviews.build]などネスト資源のフォーム作成- rating をセレクトボックスやラジオボタンで扱う例
バリデーション・エラーハンドリング
- バリデーションエラー時に同じページにレンダリングしなおすパターン
- Turbo 対応時のエラーレスポンスやステータスコードの選び方 (422 Unprocessable Entity)
認可・認証関連の話題(ある程度触れられる可能性)
- ログイン済みユーザーのみレビュー可能
- 自分のレビューしか編集・削除できない、といったポリシー例
テストのサンプル
- Minitest / RSpec いずれか、または Rails 標準のテスティングガイドに沿った形でのモデル・コントローラテスト例
- ルーティング・関連付け・バリデーションなどのテスト
要点として、このガイドは:
- Rails 8.1 で「ある程度現実的なドメイン(product reviews)」を使って
- モデル・ビュー・コントローラ・ルーティング・バリデーション・テスト
- そして場合によっては Turbo/Stimulus まで
を一通りつなげて見せる「実践的なチュートリアル」のような位置づけと思われます。
2-2. guides/source/wishlists.md の変更 (+1 / -8)
wishlists.md に対してはごく小さい修正のみです。
典型的にありえる内容は:
- Product Reviews ガイドとの整合性を取るためのリンク追加・章番号の調整
- 「次に読むガイド」として
product_reviewsへのリンクを追加 - 古いバージョン固有の記述や typo の修正・削除
行数的には 8 行削除 / 1 行追加なので、文章の差し替え・段落の統合・見出しレベルの変更など、小さな編集にとどまっています。機能仕様や API 解説のような本質的変更ではないと考えて問題ありません。
- 影響範囲・注意点
ランタイム挙動への影響:
- 変更はすべて
guides/source配下であり、Rails 本体のコードは一切変更されていません。 - そのため、アプリケーションの挙動・API・互換性への影響はありません。
- 変更はすべて
対象バージョン:
- タイトルから、「8 1 stable」ブランチ向けの cherry-pick と思われます。
- Rails 8.1 系の公式ガイドに Product Reviews チュートリアルが追加された、という位置づけです。
ドキュメント利用者への影響:
- Rails 初学者や、サンプルアプリを通じて 8.1 の構成に慣れたい開発者に有用です。
- Wishlists ガイドを既に読んでいる場合、その続き・姉妹編のような形で Product Reviews ガイドを読むことになる可能性があります。
- 実案件で reviews 機能を実装する際のモデル・コントローラ・ビュー構成の「リファレンス例」としても利用できます。
注意点としては以下程度です:
- ガイドはあくまでサンプルであり、そのまま本番環境に流用する場合は認可・バリデーション・攻撃耐性(スパムや大量投稿など)を自分の要件に合わせて補強する必要があります。
- 8.1 以外のバージョン(特に 7 系以前)では、ガイド中で前提とされている API やフロントエンド構成が異なる場合があります。
- 参考情報 (あれば)
Rails Guides 一覧(英語版):
https://guides.rubyonrails.org/類似ガイド(Wishlists など)は、公式ガイドの「Getting Started / Tutorial」系セクションにまとまっていることが多く、Product Reviews ガイドも同様のカテゴリに追加されている可能性があります。
Product Reviews ガイド内で使われているであろう要素(参考ドキュメント):
- Active Record Associations: https://guides.rubyonrails.org/association_basics.html
- Active Record Validations: https://guides.rubyonrails.org/active_record_validations.html
- Action Pack / Routing: https://guides.rubyonrails.org/routing.html
- Turbo / Hotwire (8.1 の場合、標準構成に含まれる可能性あり)
#57964 Fix casing in product reviews guide
マージ日: 2026/7/3 | 作成者: @p8
- 概要 (1-2文で)
このPRは、Rails ガイド「product_reviews.md」内の英単語の大文字・小文字表記(casing)を修正し、用語やコードの表記ゆれを整えるドキュメント修正です。機能や挙動の変更はなく、アプリケーションコードやテストには影響しません。
- 変更内容の詳細
- 対象ファイルは
guides/source/product_reviews.mdのみで、行単位で「+21 / -21」となっており、文章やコード例の一部表記が置き換えられています。 - タイトルの通り "casing" の修正が中心で、主に以下のような変更が想定されます(実際の差分から一般的に起こるパターンを整理):
- 英語の表記ゆれの統一
- 例: "Product reviews" → "Product Reviews"(ガイドタイトルや見出しとしての表記統一)
- 例: "api" → "API"、"json" → "JSON" などの一般的な頭字語の大文字化
- 例: "Postgresql" → "PostgreSQL" など、固有名詞の正しい大文字小文字への修正
- Rails 用語の表記統一
- 例: "Active record" → "Active Record"
- 例: "Action cable" → "Action Cable"
- コード例・識別子の区別を明確にするための修正
- 文中のクラス名・モジュール名を、実際の Ruby/Rails の定義と揃えた可能性があります
- 例:
productreview→ProductReview - 例:
productreviewscontroller→ProductReviewsController
- 例:
- 文章とコードの混在箇所で、コードは小文字スネークケース、クラス名はパスカルケースといった区別を明確化
- 文中のクラス名・モジュール名を、実際の Ruby/Rails の定義と揃えた可能性があります
- 英語の表記ゆれの統一
※差分から見ると行数は同じであるため、段落削除や追加ではなく、同じ行の中での文字置換(大文字小文字の変更)が中心です。
- 影響範囲・注意点
- ランタイム挙動への影響:
- ガイド文書のみの変更であり、Rails 本体のコード・API・挙動・テストには一切影響しません。
- 既存アプリケーションへの影響:
- 既存の Rails アプリやガイドに従って実装しているコードが壊れることはありません。
- 読者への影響 / 注意点:
- ガイドを参照している場合、用語やクラス名の表記が Rails 本体の実際の命名規則(クラス:
CamelCase、ファイル・テーブル:snake_caseなど)とより一致するように改善されています。 - もし過去に古いガイドの表記に合わせてクラス名などを誤って命名していた場合は、今後このガイドを参照するときには、改めて Rails の一般的な命名規則(例:
ProductReview/product_reviews/product_reviews_controller.rb)に合わせることを推奨します。
- ガイドを参照している場合、用語やクラス名の表記が Rails 本体の実際の命名規則(クラス:
- 参考情報 (あれば)
- この PR には CHANGELOG への追記はなく、Rails 的には「ドキュメントの軽微な修正」と位置づけられています。
- Rails ガイドと実際の API/クラス名を揃えることは、チュートリアルどおりに書いたのにエラーになる、といった学習者の混乱を防ぐ効果があります。
- Rails の命名規則の詳細は以下を参照するとよいです(英語):
- Rails Guides – Naming Conventions: https://guides.rubyonrails.org/active_record_basics.html#naming-conventions
#57244 [RF-DOCS][ci-skip] Add Product Reviews Tutorial
マージ日: 2026/7/3 | 作成者: @excid3
- 概要 (1-2文で)
このPRは、Rails公式ガイドの「ECサイトチュートリアル」の続きとして「商品レビュー(Product Reviews)」機能を実装する新しいチュートリアルを追加するドキュメント変更です。ユーザーによる星評価付きレビュー、画像アップロード、レビューに基づく商品の平均評価更新、フィルタリング・集計表示、管理画面でのレビュー編集などを一通りカバーしています。
- 変更内容の詳細
※コードベース本体ではなく「ガイド(Documentation)」のみの変更です。新規に product_reviews.md が追加され、既存の wishlists.md がわずかに修正されています。
2-1. 新チュートリアル: guides/source/product_reviews.md
このチュートリアルは、既存の「ECストア」チュートリアルに続けて「商品レビュー機能」を実装する流れを詳細に説明しています。主な要素は以下です。
モデル構成と関連付け
典型的には次のようなモデル構造を前提として解説されます:
Product- 多数の
Reviewを持つ - 平均評価(例:
ratingやaverage_ratingカラム)を持ち、レビュー投稿・更新のたびに再計算
- 多数の
Reviewbelongs_to :productbelongs_to :user(レビュアーを紐づけるケースが多い)- 属性例:
rating(1〜5の整数)body(本文)- 画像添付用の
has_many_attached :imagesなど (Active Storage を使用した例が多い)
サンプルイメージ:
class Product < ApplicationRecord
has_many :reviews, dependent: :destroy
def update_rating!
update!(
rating: reviews.average(:rating).to_f.round(1) # 小数1桁など
)
end
end
class Review < ApplicationRecord
belongs_to :product
belongs_to :user
has_many_attached :images
validates :rating, presence: true, inclusion: { in: 1..5 }
after_commit :update_product_rating
private
def update_product_rating
product.update_rating!
end
endコントローラとルーティング
レビューは、商品にネストしたリソースとして扱う構成が説明されます。
# config/routes.rb
resources :products do
resources :reviews, only: [:create, :edit, :update, :destroy]
endReviewsController では、以下のような操作をカバーします。
- レビュー作成 (
create) - レビュー編集・更新 (
edit/update) - レビュー削除 (
destroy)(場合によっては管理者のみ)
class ReviewsController < ApplicationController
before_action :set_product
before_action :set_review, only: [:edit, :update, :destroy]
before_action :authenticate_user!
def create
@review = @product.reviews.new(review_params.merge(user: current_user))
if @review.save
redirect_to @product, notice: "レビューを投稿しました。"
else
render "products/show", status: :unprocessable_entity
end
end
# ...
private
def set_product
@product = Product.find(params[:product_id])
end
def set_review
@review = @product.reviews.find(params[:id])
end
def review_params
params.require(:review).permit(:rating, :body, images: [])
end
end星評価入力(UI)とCSS
説明の中で特徴的なのが「星評価をラジオボタンで実装し、CSSで直感的な星アイコンにする」という点です。これにより、以下の利点があります。
- アクセシビリティを確保(スクリーンリーダーやキーボード操作に対応)
- フォーム送信時には通常の
ratingパラメータとして送られる - CSSのみで視覚的にはリッチな星評価UIを実現
フォーム例:
<%= form_with model: [@product, @review] do |f| %>
<div class="rating-input">
<% 5.downto(1) do |i| %>
<%= f.radio_button :rating, i, id: "review_rating_#{i}" %>
<%= f.label :"rating_#{i}" do %>
★
<% end %>
<% end %>
</div>
<%= f.text_area :body %>
<%= f.file_field :images, multiple: true %>
<%= f.submit "レビューを投稿" %>
<% end %>CSSで label を星形にスタイリングし、input:checked ~ label などのセレクタを使って、選択状態に応じて星の色が変わるようにする、というパターンがチュートリアルで解説されています。
商品の平均評価の自動更新
レビュー作成・更新・削除のたびに、Product 側の評価を自動で再集計する仕組みを解説しています。
- コールバック (
after_commitなど)でproduct.update_rating!を呼び出す - SQL の
AVG(rating)を利用して平均値を計算 Productモデルのratingカラムに保存しておくことで、一覧画面などで N+1 クエリを避ける
計算例:
def update_rating!
update!(
rating: reviews.average(:rating) || 0
)
endレビューの絞り込みと集計表示
商品詳細ページなどで、レビューを以下のように表示・集計する例が解説されています。
- 指定した星数のみを表示するフィルタ (例: 「星5のみ」「星4のみ」)
params[:rating]をもとに@product.reviews.where(rating: params[:rating])などで絞り込み
- 星ごとのレビュー数・割合をバーグラフで表示
- 例: 5〜1まで各星の件数をカウントし、全体に対する割合を計算
# Product モデル例
def rating_breakdown
total = reviews.count.to_f
return {} if total.zero?
(1..5).each_with_object({}) do |star, hash|
count = reviews.where(rating: star).count
hash[star] = {
count: count,
percent: ((count / total) * 100).round
}
end
endビュー側例 (擬似コード):
<% @product.rating_breakdown.sort.reverse_each do |star, data| %>
<div class="rating-row">
<span><%= star %>★</span>
<div class="bar">
<div class="bar-fill" style="width: <%= data[:percent] %>%"></div>
</div>
<span><%= data[:count] %>件 (<%= data[:percent] %>%)</span>
</div>
<% end %>画像アップロード(任意)のサポート
- Active Storage を使った画像添付例が含まれています
- 1レビューに複数画像を添付できるように
has_many_attached :imagesを使用 - ビューで
image_tagを使ってプレビュー表示する例が示されます
class Review < ApplicationRecord
has_many_attached :images
end<% @review.images.each do |image| %>
<%= image_tag image.variant(resize_to_limit: [400, 400]) %>
<% end %>管理画面でのレビュー管理
- 管理者がレビューを編集・削除できるようにするための UI / コントローラが説明されています
- 主な用途: 誤字修正、不適切表現の修正、スパム的レビューの削除など
- 実装例:
namespace :admin配下にAdmin::ReviewsControllerを作成- または既存の管理UI(例:
Admin::ProductsController)にネストして操作する
ルート例:
namespace :admin do
resources :products do
resources :reviews, only: [:index, :edit, :update, :destroy]
end
end権限管理の考慮(認証・認可)が軽く触れられている可能性があります(before_action :require_admin! など)。
2-2. guides/source/wishlists.md の微修正
wishlists.md は既存のECストアチュートリアルの別パートで、ここに対して:
- 他パート(今回追加された
product_reviews.mdなど)へのリンクの修正・追加 - あるいは構成の一貫性を保つための軽微な文章・見出しの変更
が行われています(+1/-8 行なので、ごく小さな調整に留まっています)。
- 影響範囲・注意点
影響範囲
- Rails本体のコード・APIには影響せず、公式ガイド(ドキュメント)のみの変更です。
- ただし「Rails Way」でのECサイトにおけるレビュー機能の実装パターンとして、今後このチュートリアルが事実上の「推奨例」になる可能性があります。
- 特に以下の設計方針が、Railsコミュニティにおけるデファクトな例として参照されやすくなります:
- 星評価をラジオボタン + CSS で実装するアプローチ
- モデルのコールバック(orサービスオブジェクト)で平均評価を自動更新するアプローチ
- Active Storage を利用したレビュー画像添付
注意点 / 実装上のポイント
- パフォーマンス:
- 大量レビューがある商品の平均値再計算は、
reviews.average(:rating)を毎回呼び出すと高コストになる可能性があります。- チュートリアルではシンプルな実装を重視しているため、実運用ではカウンタキャッシュ的な集計(
reviews_count,rating_sumなど)で最適化を検討するとよいです。
- チュートリアルではシンプルな実装を重視しているため、実運用ではカウンタキャッシュ的な集計(
- 大量レビューがある商品の平均値再計算は、
- 整合性:
- レビュー削除時にも平均値の更新が走るように、
after_commitなどのコールバックの種類やタイミングには注意が必要です。
- レビュー削除時にも平均値の更新が走るように、
- セキュリティ:
- 画像アップロードでは、ファイルサイズ・拡張子・Content-Typeの制限や、サムネイル生成時のImageMagick/ libvips 周りの安全性確保など、実運用では追加対策が必要になります。
- 権限/認可:
- 一般ユーザーが自分のレビューだけを編集・削除できるようにするのか、管理者のみ許可するのか、チュートリアルは1つの例に過ぎないため、自プロジェクトではポリシーを明確にする必要があります。
- パフォーマンス:
- 参考情報 (あれば)
Rails Guides (今後このチュートリアルが掲載されるであろう場所):
https://guides.rubyonrails.org/
(Product ReviewsもしくはE-Commerce関連のチュートリアルセクションに追加される想定)Active Storage ガイド(画像アップロードの詳細):
https://edgeguides.rubyonrails.org/active_storage_overview.htmlRails公式チュートリアル内の他のEC機能 (カート、ウィッシュリストなど) と合わせて読むと、フル機能のECサイト構築フローとして参考になります。
#57826 Deprecate Mime::SET, Mime::LOOKUP and Mime::EXTENSION_LOOKUP
マージ日: 2026/7/2 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails の MIME タイプ周りの内部レジストリを直接公開していたMime::SET,Mime::LOOKUP,Mime::EXTENSION_LOOKUPが非推奨化され、代わりに既存の公開 API(Mime.symbols,Mime[...],Mime::Type.lookup,Mime::Type.lookup_by_extensionなど)と新設のMime.extensionsを使うように誘導する変更です。
これにより、ミュータブルな内部状態への直接アクセスを抑制しつつ、これまで内部定数でしかできなかった操作も正式なパブリック API でカバーされるようになりました。
- 変更内容の詳細
非推奨になった定数
以下の 3 つの定数が非推奨になり、ActiveSupport::Deprecation::DeprecatedObjectProxy でラップされるようになりました。
Mime::SET- MIME タイプのレジストリ(シンボルなど)を保持していたセット。
Mime::LOOKUP- 文字列表現(例:
"text/html")から MIME タイプを引くためのハッシュ。
- 文字列表現(例:
Mime::EXTENSION_LOOKUP- 拡張子(例:
"html")から MIME タイプを引くためのハッシュ。
- 拡張子(例:
これらはまだ参照可能ですが、使用すると deprecation warning が出るようになっています。
推奨される置き換え API
PR 説明にある通り、用途ごとに次のように書き換えが推奨されます。
1) Mime::SET
用途: 登録済みの MIME タイプを列挙・取得したい場合
置き換え:
- 登録済みの MIME シンボル一覧が欲しいときruby
# 旧 Mime::SET # 非推奨 # 新 Mime.symbols - MIME タイプをキー(シンボルや文字列)から取得したいときruby
# 旧 type = Mime::SET[:html] # など # 新(推奨) type = Mime[:html] # または type = Mime::Type.lookup("text/html")
2) Mime::LOOKUP
用途: "text/html" のような MIME 文字列から Mime::Type を取得
置き換え:
# 旧
Mime::LOOKUP["text/html"] # 非推奨
# 新
Mime::Type.lookup("text/html")3) Mime::EXTENSION_LOOKUP
用途: ファイル拡張子 "html" から MIME タイプを取得したり、全拡張子を列挙したり
置き換え:
拡張子から MIME タイプを取得
ruby# 旧 Mime::EXTENSION_LOOKUP["html"] # 非推奨 # 新 Mime::Type.lookup_by_extension("html") # または Mime[:html] # 既存のショートカット登録済み拡張子を列挙したい(従来は
Mime::EXTENSION_LOOKUPでしかできなかった)
今回、新しくMime.extensionsが追加されています。rubyMime.extensions # => [:html, :text, :json, ...] (全ての拡張子・シノニムを含む)
これにより、「レジストリ(Hash/Set)自体」に触るのではなく、メソッド経由で取得・列挙するスタイルに統一されます。
コア実装の変更点(概要)
actionpack/lib/action_dispatch/http/mime_type.rb の変更が中心で、以下のような対応がされています。
Mime::SET,Mime::LOOKUP,Mime::EXTENSION_LOOKUPを内部実体に対するDeprecatedObjectProxyに差し替え- 実行時には従来通り動作するが、アクセスごとに deprecation warning を発生させる。
Mime.extensionsの追加Mime.symbols同様の公開メソッドとして、拡張子の一覧取得をサポート。
- 既存内部コードで、これら定数を直接使っていた箇所を公開 API ベースの呼び出しにリファクタリング
action_mailer.rb,action_dispatch.rb,abstract_controller/collector.rbなど。
テストの更新
actionpack/test/dispatch/mime_type_test.rbなどで:- 新メソッド
Mime.extensionsの挙動テストが追加。 - 旧定数使用時に deprecation が発生することの確認テストなどが追加または修正。
- 新メソッド
actionview/test/template/lookup_context_test.rbなど内部で MIME レジストリを使うテストも、新 API に合わせて微修正。
- 影響範囲・注意点
影響を受けるコード
以下に該当するアプリケーション/ライブラリは deprecation warning の対象になります。
Mime::SET/Mime::LOOKUP/Mime::EXTENSION_LOOKUPを直接参照しているコード- 例:ruby
Mime::SET.each do |type| # ... end if Mime::EXTENSION_LOOKUP["csv"] # ... end
- 例:
これらは Rails の内部実装詳細(ミュータブルなレジストリそのもの)に依存しているため、今後のメジャーバージョンで削除される可能性が高いです。
マイグレーションの指針
- 列挙用途
- MIME シンボル一覧:
Mime.symbols - 拡張子一覧:
Mime.extensions
- MIME シンボル一覧:
- ルックアップ用途
- MIME 文字列
"text/html"→Mime::Type.lookup("text/html") - 拡張子
"html"→Mime::Type.lookup_by_extension("html")またはMime[:html]
- MIME 文字列
- レジストリを直接変更している場合(かなりレアケース)
- 例えば
Mime::EXTENSION_LOOKUP["pdf"] = ...のように書いている場合は、元々非推奨なパターンであり、公式な登録 API(Mime::Type.registerなど)または提供されている拡張メソッド経由に置き換えるべきです。
- 例えば
注意点
- 今はまだ動きますが、DeprecatedObjectProxy 経由の使用は将来の削除を前提とした一時措置です。
deprecation メッセージが CI などで検出される場合は、早めの対応が必要です。 - 特に gem やエンジンなどのライブラリは、利用者のアプリケーションで deprecation noise を出さないよう、できるだけ早期に新 API へ移行しておくとよいです。
- 参考情報 (あれば)
- 置き換え API 一覧:
Mime::SET- →
Mime.symbols,Mime[...],Mime::Type.lookup
- →
Mime::LOOKUP- →
Mime::Type.lookup
- →
Mime::EXTENSION_LOOKUP- →
Mime.extensions,Mime::Type.lookup_by_extension,Mime[...]
- →
- 関連ファイル:
actionpack/lib/action_dispatch/http/mime_type.rb(MIME 周りの中核実装)actionpack/CHANGELOG.md(非推奨化の公式なアナウンスが追記済み)
#57917 Attribute methods ractor safe
マージ日: 2026/7/2 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のActiveModel::AttributeMethods周りで使われるaliases_by_attribute_name/attribute_aliases/attribute_method_patternsを、Ractor 間で安全に共有できるよう「デフォルトで freeze される値オブジェクト&コレクション」に変更した PRです。これにより、Ractor 環境でも属性メソッド関連の情報を安心して参照できるようになり、並行実行時のデータ競合や Ractor 制限によるエラーを防ぎます。
- 変更内容の詳細
2-1. 目的となる3つのメソッドの Ractor-safe 化
Ractor 間で共有するには、「凍結されていて、かつ Ractor-shareable なオブジェクト」である必要があります。この PR では以下をその条件に合わせるための変更が行われています。
AttributeMethods.aliases_by_attribute_nameAttributeMethods.attribute_aliasesAttributeMethods.attribute_method_patterns
方針としては:
- 値オブジェクトおよび配列・ハッシュを「生成時に freeze」する
- 「必要になった時に書き換える(遅延・破壊的変更)」スタイルを避ける
- 継承時に子クラスへ「凍結済みのコピー/設定」を行っておく
という構造になっています。
2-2. AttributeMethodPattern の値オブジェクトをデフォルトで frozen に
AttributeMethodPattern は、属性メソッド (before_type_cast, ?, = など) を表現するための内部的な値オブジェクトです。
この PR での変更点:
AttributeMethodPatternのインスタンスを生成時にfreezeするように変更- これにより、
attribute_method_patternsが保持するパターンの配列全体が Ractor 間で共有可能になる
イメージとしては:
pattern = AttributeMethodPattern.new(prefix: "", suffix: "=")
pattern.frozen? # => trueのように、パターン自体に後から変更を加えない前提をコードで保証するようにしています。
2-3. aliases_by_attribute_name を「継承時に」初期化するよう変更
従来は、aliases_by_attribute_name は「最初にアクセスされたときに default_proc を使って遅延生成・更新」していた箇所がありました。これは「ハッシュにアクセスした瞬間に中身を書き換える」ため、Ractor から見ると共有が難しくなります。
この PR では:
aliases_by_attribute_nameを クラス継承時に確定させる 形に変更
→ サブクラス用のハッシュを、親クラスからコピー・設定するタイミングを明示的にdefault_proc内でハッシュを書き換えるのをやめ、「必要なキーが無い場合は、凍結された空配列などを返すだけ」に変更
イメージ:
# 旧: キー未定義時に default_proc 内でハッシュを更新していた
hash.default_proc = ->(h, k) { h[k] = [] }
# 新: ハッシュ自体は固定し、見つからないキーのときは
# 単に空の frozen 配列を返すだけにする
hash.default_proc = ->(_h, _k) { EMPTY_FROZEN_ARRAY }こうすることで、aliases_by_attribute_name 自体は「それ以上変更されないハッシュ」となり、Ractor 間で共有しても安全になります。
2-4. 「欠けているキー」に対しては凍結された配列を返す
aliases_by_attribute_name[attribute_name] が存在しない場合:
- 以前は default_proc 内で
hash[attribute_name] = []のように書き換えつつ空配列を返していた - 現在は、ハッシュを変更せず、単に「中身の無い・凍結された配列」を返す
これにより:
- 「読み取りのみ」のパターンになる
- 返される配列も frozen なので、Ractor 間で共有しても破壊的変更ができない設計になる
2-5. attribute_aliases / attribute_method_patterns の扱い
attribute_aliases も attribute_method_patterns も、本質的には「定義時に決まって、その後あまり変えたくない情報」です。
この PR により:
- それぞれの戻り値となるハッシュや配列がデフォルトで
freezeされるよう整理 - (
AttributeMethodPattern自体も frozen なので)attribute_method_patterns全体が Ractor-safe になる
Rails アプリ側から見ると、これらに対して「返ってきた配列/ハッシュを破壊的に変更する」コードは元々非推奨に近い使い方ですが、この変更によって 実際に破壊的操作ができなくなる(=例外になる) ため、Ractor 対応と同時に API の利用スタイルもより明示的になります。
- 影響範囲・注意点
3-1. 影響を受ける主なコード
ActiveModel::AttributeMethodsを継承して独自の attribute メタプログラミングをしているクラスattribute_method_patterns,attribute_aliases,aliases_by_attribute_nameを直接触っているメタプログラミング系コードや gem
特に注意が必要なのは次のようなコードです。
NG になりうるパターン例
# attribute_aliases に破壊的変更を加える
MyModel.attribute_aliases[:foo] = :bar
# or
MyModel.attribute_method_patterns << some_patternこれらは返り値が frozen になることで FrozenError になる可能性があります。
3-2. 対処法の方針例
返ってきたオブジェクトを「そのまま破壊的に変更する」代わりに:
dup/to_h/to_aしてから新しいオブジェクトを作成し、- 専用の設定メソッドや API を通じて反映する
といった形が推奨されます。
例:
# 旧: 直接破壊的に変更
MyModel.attribute_aliases[:foo] = :bar
# 新: 専用 API か、初期定義時にまとめて設定する
MyModel.attribute_alias(:foo, :bar)attribute_method_patterns も基本的には「クラス定義時にパターンを追加する」ための public API 経由で触るべきで、内部の配列を直接いじるスタイルは避けるとよいです。
3-3. Ractor を使わない場合でも影響はあるか
- Ractor を使わないアプリでも、「内部構造が freeze されるようになった」という意味で影響があります
- ただし、通常の利用(public API 経由で attribute alias やメソッドパターンを定義する)であれば実害はなく、テストも追加されているため後方互換性は基本的に保たれる想定です
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57917
- Ractor の shareable オブジェクト要件(Ruby 3+)
freezeされていること- 参照グラフ全体が shareable であること
- Rails 内の関連コード
ActiveModel::AttributeMethods(属性メソッド定義のベース)ActiveRecord::Baseはこれを継承しているため、全 AR モデルに波及する仕組み
この PR により、 attribute 周りの内部メタデータ構造が「読み取り専用」設計に近づき、Ractor による並行実行が現実的に扱いやすくなっています。
#57757 Make InheritableOptions ractor safe
マージ日: 2026/7/2 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のInheritableOptions(ActiveSupport::OrderedOptions系)の実装を見直し、freeze済みで Ractor 共有可能(Ractor-safe)にできるようにした PRです。親ハッシュへのフォールバックを行うデフォルト Proc を凍結時に除去し、Ractor の shareable 制約に抵触しない仕組みに変更しています。
- 変更内容の詳細
背景: なぜ Ractor-safe ではなかったか
InheritableOptions は、親オプションを継承しつつ子側で上書きできる仕組みで、内部的には「キーが見つからない時に親ハッシュを見る」ためのデフォルト Proc を持っています。
イメージとしては以下のような構造です:
class InheritableOptions < ActiveSupport::OrderedOptions
def initialize(parent = nil)
@parent = parent
default_proc = ->(hash, key) { @parent[key] if @parent && @parent.key?(key) }
super(&default_proc)
end
end- この
default_procはインスタンス変数@parentをキャプチャするため、- Proc 自体が Ractor 的に「shareable」になりにくい
- かつ、その Proc を持つオプションオブジェクトも shareable にできない
Ractor.make_shareableは深くオブジェクトグラフを辿るので、「freezeしたら勝手に@parentまで shareable 化する」のは副作用が大きく、freezeの責務としても不適切
そのため、「freeze 済み InheritableOptions を Ractor に渡す」ということが安全にできない状態でした。
方針: freeze 時に親参照を解消する
この PR では、
「凍結前は親を参照するが、凍結するときに親からのフォールバックを解消し、単純なハッシュとして完結させる」
という方針を取っています。
具体的には:
InheritableOptions#freezeが呼ばれた時に- 親ハッシュの内容を「reverse merge」の形で自分自身に取り込む
- =「自分に値がなければ親の値をコピーする」
- 親ハッシュの内容を「reverse merge」の形で自分自身に取り込む
- そのうえで
default_procを捨てる(親を参照しないようにする) - 結果として、凍結後は「見た目は同じ値を返すが、親にアクセスしない純粋な hash-like オブジェクト」になる
擬似コードで表現すると次のようなイメージです(実装は ordered_options.rb 内):
def freeze
unless frozen?
if @parent
# 親の値を自分自身に reverse_merge しておく
@parent.each do |k, v|
self[k] = v unless key?(k)
end
end
# 親を参照する default_proc を除去
self.default_proc = nil
super
end
end※実際のコードでは OrderedOptions / InheritableOptions の構造に合わせた実装になっています。
overridden? の実装変更
InheritableOptions には概ね次のようなメソッドが存在します:
options.overridden?(:foo) # 親から継承したのではなく、子で明示的に設定されているか?従来は「子にキーが存在するかどうか」で判定できていましたが、この PR 後は凍結時に親のキーも子へコピーされるため、そのままだと「すべて overridden 扱い」になってしまいます。
そこで、この PR では:
- 「どのキーが子で明示的に設定されたか」を内部で追跡するように変更
- 例:
@overridden_keysのようなセットに、値を代入したタイミングでキーを記録
- 例:
overridden?(key)は「@overridden_keysにそのキーが含まれているか」で判定するように変更
これにより、
- 凍結前後で
overridden?の意味・結果を変えずに維持 - かつ凍結後は「親キーが子へコピーされても、それだけでは overridden と判定されない」
という動作にできます。
テスト追加
activesupport/test/ordered_options_test.rb にテストが追加されており、主に以下を検証しています:
- Ractor-safe 化された
InheritableOptionsがこれまでどおりの値解決を行うこと freeze前後でoverridden?の結果が変わらないこと- 親オプション・子オプションの関係が、凍結後も期待通りに維持されること(挙動レベルで)
- 影響範囲・注意点
影響範囲
ActiveSupport::InheritableOptionsを直接/間接的に利用しているコード全般- 特にエンジンや Railtie の設定オプションなど、「親設定を継承して子で上書きする」パターン
overridden?を利用しているコード(Gem/アプリのメタプログラミング的な部分など)
互換性
- 通常ユースケースでは、凍結前後で返却される値は従来と一致するよう配慮されているため、挙動的な非互換は最小限
overridden?が「内部実装として、単にkey?か否かに依存している」と仮定していたような非常に内部寄りのコードがあれば、影響を受ける可能性はありますが、そのようなコードは稀と考えられます。
Ractor を使う場合の注意
- この PR により、「
freeze済み InheritableOptions を Ractor に渡す」パターンが実現しやすくなります。 - ただし、親オプション自体や、親にぶら下がる値オブジェクトが Ractor-safe かどうかは別問題であり、アプリ側で引き続き注意が必要です。
freezeはあくまで「親からのフォールバックを解消し Proc を除去する」だけであり、Ractor.make_shareableを内包してはいません。そのため、凍結=完全 shareable という保証ではない点には留意してください。
- この PR により、「
- 参考情報 (あれば)
- PR 本文の論点:
- Ractor の shareable 要件と
default_proc(Proc のクロージャ)が持つ参照の問題 freezeで勝手にRactor.make_shareableを呼ぶことの是非(副作用の大きさと API としての責務不一致)
- Ractor の shareable 要件と
- 関連するクラス/概念:
ActiveSupport::OrderedOptionsActiveSupport::InheritableOptions- Ractor /
Ractor.make_shareable
- 実際に Rails アプリを Ractor-safe にする場合は、他のコンポーネント(例えば logger、キャッシュストア、クラス変数に保持している設定値など)も含めて「Proc やミューテーブルなオブジェクトがどこに残るか」を総合的に見ていく必要があります。
#57953 Mark a connection unverified when a non-StandardError interrupts a query
マージ日: 2026/7/2 | 作成者: @edaroit
- 概要 (1-2文で)
- Active Record の DB コネクションプールで、
StandardError以外の例外(Timeoutや fiber scheduler のAsync::Stop/Async::Cancelなど)でクエリが中断された場合にも、そのコネクションを「未検証状態」にマークするようにし、後続リクエストに壊れたソケットが再利用されないようにする修正です。 - 中断されたクエリ自体の挙動(例外がそのまま飛ぶ)は変えず、「プールに戻されたコネクションの信頼性」だけを正しく扱うことで、EOFError などが後続の無関係なリクエストで発生するバグを防ぎます。
- 変更内容の詳細
背景となる問題
ConnectionAdapters::AbstractAdapter#with_raw_connection は内部で DB に対して「生の」クエリ実行を行い、ここで例外が起きたときにコネクション状態を調整しています。
従来の挙動:
def with_raw_connection(...)
# 省略
rescue => e # == rescue StandardError
downgrade_connection_after_error(e)
raise
endrescue => eは Ruby の仕様上StandardErrorだけを捕捉します。Timeoutや、多くの async/fiber ランタイム (Async::Stop,Async::Cancel) はExceptionの直下 or 途中のサブクラスであり、StandardErrorの外側です。- そのため、クエリ実行中にそうした例外で中断されると:
- 例外は
with_raw_connectionのrescueをすり抜けて外側へ伝播 - しかし、そのタイミングでコネクションオブジェクトは「正常・検証済み」としてプールに戻されてしまう
- 次にプールからこのコネクションを checkout したリクエストが、破綻したソケットを使おうとして
EOFErrorやActiveRecord::ConnectionFailedを受ける
→ 一つのリクエストの中断が、後続の無関係なリクエストを壊す
- 例外は
特に Falcon など fiber scheduler ベースのサーバでは、キャンセルを fiber 内に Async::Stop として直接投げ込むため、このパスに頻繁に乗りやすくなります。
追加された処理
このギャップを塞ぐため、with_raw_connection に rescue Exception を追加し、「StandardError 外の例外でもコネクションを“ダウングレード”」するようにしました。
追加コード(概略):
def with_raw_connection(...)
# 通常の処理
rescue => e # StandardError 系
downgrade_connection_after_error(e)
raise
rescue Exception
@last_activity = nil
@verified = false
raise
endここで重要なのは:
@verified = false@last_activity = nil
の2つの書き込みを行うことです。
なぜ @verified = false だけでは足りないのか
checkout 時の再検証ロジックはおおよそ次のようになっています:
elsif !@needs_reconnect && (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
# 直近で使われているなら、検証をスキップしてよし、とみなすこの分岐は「@needs_reconnect が false で、かつ最近使われた(last_activity が小さい)」場合に、検証(verify!)をスキップします。
@verified = falseだけ変えても、- 「最近使われた」扱いだとこのショートカットに乗ってしまい、
- 実際には壊れているソケットを再検証せずに再利用してしまう可能性があります。
- そこで
@last_activity = nilを一緒にクリアすることで、- 「最近使われた」ショートカットに入らないようにし、
- 次の利用時に必ず検証経路に入り、そこで死んだソケットなら再接続されるようにしている。
なぜ @needs_reconnect は触らないのか
downgrade_connection_after_error は、エラーを「再試行可能な接続エラー」と判定できる場合にだけ @needs_reconnect = true を設定します。
- 今回対象の
Exception系の割り込みは、- DB ドライバのエラー分類の範囲外であり、
- 「接続エラーである」と決め打ちできる根拠がない。
- ただし
@verified/@last_activityをクリアすれば、次の利用時に verify が走り、そこで本当に再接続が必要なら@needs_reconnectが立つ or 実際に reconnect が行われます。
そのため、このパスでは「“とりあえずこのコネクションは信用しない” という最低限のフラグリセット」に留めています。
rescue Exception への懸念と既存の前例
rescue Exception は一般にアンチパターンとされますが、このファイルでは既に類似の用途に使われており、今回の変更もそれに倣っています。
attempt_configure_connectionではTimeout::ExitException(Exception派生)を補足するためにrescue Exceptionを使用済み。- 今回も「アプリケーションコードの例外を飲み込む」のではなく、
- 捕捉したら状態だけ直して
raiseし直す、という「透明な」挙動です。 - 例外の型やスタックトレースは変えません。
- 捕捉したら状態だけ直して
テストの追加
activerecord/test/cases/adapter_test.rb に以下のようなテストが追加されています(概要):
- あるアダプタに対して:
- まず普通のクエリを投げてコネクションを「使用済み・verified な状態」にする。
- 次に
with_raw_connectionの中でStandardError外の例外を発生させる。 - その後のコネクション状態として:
@verifiedが false になっていること@last_activityが nil になっていること をアサート。
このテストは、修正がないと失敗し(コネクションが verified のまま)、修正後は通ることが確認されています。sqlite3 と trilogy で検証済みです。
- 影響範囲・注意点
- 対象:
- Active Record 全般の DB 接続処理。
- 特に fiber scheduler(Falcon, async-http 等)や、
Timeoutをクエリ実行中に使う環境で恩恵が大きいです。
- 期待される改善:
- 非
StandardErrorの例外でクエリが中断された後に、別のリクエストで突然EOFErrorやActiveRecord::ConnectionFailedが起きる、といった「後続リクエストへの汚染」が起きにくくなります。 - プール内のコネクションの「整合性」が高まり、障害の局所化がしやすくなります。
- 非
- 互換性・副作用:
- 中断されたリクエスト側の例外の種類・伝播のされ方は変わりません(あくまでコネクション状態のフラグを書き換えるだけ)。
- 非
StandardErrorを故意にクエリ中断に使っている場合、次回そのコネクション利用時に verify が必ず走るようになるため、ごく軽微ですが再接続や追加の round-trip が増える可能性があります。 rescue Exceptionを追加していますが、即座に例外を再送出しており swallowing はしていません。挙動面の変化は「コネクションが unverified になるかどうか」だけです。
- 参考情報 (あれば)
- 該当コード(verify スキップ条件):
https://github.com/rails/rails/blob/b4379f0180a0b7f3e8e5e390c97e9586d556d817/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L1105-L1106 - 類似の
rescue Exception使用箇所:attempt_configure_connection(Timeout::ExitException対応) - 関連するエラー例:
EOFErrorActiveRecord::ConnectionFailedAsync::Stop,Async::Cancel(fiber scheduler のキャンセル用例外)
#57957 Bump Github Action cache version to 6
マージ日: 2026/7/1 | 作成者: @akhilgkrishnan
- 概要 (1-2文で)
Rails が生成する GitHub Actions ワークフロー(CI 用)の「キャッシュバージョン」を 6 に更新した PRです。アプリケーション/プラグイン用 generator が出力する.github/workflows/ci.ymlのテンプレートのみが対象です。
- 変更内容の詳細
対象ファイル:
railties/lib/rails/generators/rails/app/templates/github/ci.yml.ttrailties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
これらは rails new や rails plugin new 実行時に .github/workflows/ci.yml として生成されるテンプレートです。
今回の変更は、GitHub Actions のキャッシュキーに使っている「内部バージョン番号」を 5 → 6 に bump しただけの非常に小さな変更です。
イメージとしては、テンプレート中に以下のような行があるとします(実際の PR でもこれに近い形です):
- uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-v6-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-v6-この v6 の部分が以前は v5 だったものを更新しています。
この「キャッシュバージョン」は、キャッシュの破棄や再生成のタイミングをコントロールするための任意のラベルで、
- キャッシュスキーマを変えた
- 中に入れるパス構成を変えた
- bundle の扱いを変えた
といったときに bump する、という運用がよくされています。
PR 説明にある followup: https://github.com/rails/rails/pull/56772 で行われた CI テンプレート関連変更(例: Bundler 周りや Ruby セットアップ周りの見直し)に続く整備として、キャッシュのバージョンだけを更新したものと考えられます。
- 影響範囲・注意点
影響範囲
- これから
rails new/rails plugin newで作成されるプロジェクト・プラグインのうち、--skip-githubせず GitHub CI を生成するケースにのみ影響します。 - 既存プロジェクトのワークフローは自動では書き換わらないため、この PR だけでは既存リポジトリには影響しません。
- これから
効果
- 新しく生成された
.github/workflows/ci.ymlは「バージョン 6」のキャッシュを使うので、過去に「バージョン 5」などのキーで作られていたキャッシュとは衝突せず、新しい条件でクリーンなキャッシュを作り直せるようになります。 - Bundle パスや Ruby セットアップの変更に不整合がある古いキャッシュを誤って使い続けるリスクを減らします。
- 新しく生成された
注意点 / 既存プロジェクトで追従したい場合
- Rails 7.2 以降の CI テンプレートに合わせたい場合は、既存の
.github/workflows/ci.ymlで、同様にキャッシュのキー中のバージョン(v5など)をv6に手動で変更することが推奨されます。 - 変更後の最初のワークフロー実行では、新しいキーでキャッシュが作り直されるため、1 回目だけは依存関係のインストールにやや時間がかかる可能性があります。
- Rails 7.2 以降の CI テンプレートに合わせたい場合は、既存の
- 参考情報 (あれば)
- 対応する前 PR(テンプレートや CI 設定の調整などが含まれる可能性あり)
- Rails の GitHub CI テンプレートは
railtiesの generator で管理されており、ci.yml.ttがrails new時に展開されます。- app:
railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt - plugin:
railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
- app:
#57939 Apply source mappings to dynamic permissions policy sources
マージ日: 2026/7/1 | 作成者: @55728
- 概要 (1-2文で)
Permissions-Policy で「動的(callable)なソース指定」にキーワード(:selfなど)を返した場合も、静的指定と同様にマッピング('self'などへの変換)が適用されるようになりました。これにより、これまで動的ソースだけ不正なポリシー文字列(selfのような裸の単語)が出力されていた問題が修正されています。
- 変更内容の詳細
何が問題だったか
Permissions-Policy(旧 Feature-Policy)の設定で、ブロック(→ Proc)などを使って「動的に」ソースを返すケースがあります。
policy.geolocation -> { :self }このとき、:self のような「キーワードシンボル」を返しても、
静的指定:
policy.geolocation :self
→geolocation 'self'動的指定:
policy.geolocation -> { :self }
→ 以前はgeolocation self(クォートなし)になっていた
つまり、動的な場合だけ :self などのキーワードに対するマッピング処理が走らず、「素の self」が出力されてしまい、Permissions-Policy の文法上は不正なヘッダになっていました。
今回の修正内容
action_dispatch/http/permissions_policy.rb 内で、動的ソースの評価結果にも「ソースマッピング(:self → 'self' など)」を適用するように変更されています。
- 静的ソースと同じ経路でマッピングされるようになり、CSP(Content Security Policy)の実装と挙動が揃えられました。
- テスト (
actionpack/test/dispatch/permissions_policy_test.rb) が追加され、動的ソースに対しても'self'のように適切にクォートされた値が出力されることを検証しています。
挙動の具体例
修正前:
policy.geolocation :self
# => geolocation 'self'
policy.geolocation -> { :self }
# => geolocation self # クォートなしで不正修正後:
policy.geolocation :self
# => geolocation 'self'
policy.geolocation -> { :self }
# => geolocation 'self' # 静的と同じマッピングが適用される他のキーワード(例: :none など)についても同様に、静的指定時と同じルールでマッピング・整形されます。
- 影響範囲・注意点
- 影響を受けるのは、Permissions-Policy で ブロック / Proc などの動的ソース指定を使い、かつ
:selfなどのキーワードシンボルを返している場合 です。 - これまではそのようなコードが不正なヘッダ(
selfのようにクォートされていない単語を含む)を生成していた可能性があり、ブラウザによってはヘッダ全体が無視されていた可能性があります。 - この修正によりヘッダが正しく解釈されるようになり、以前は実質無効化されていた Permissions-Policy が今後は有効に適用されるようになることがあります。その結果、既存アプリで以下のような挙動変化が起こり得ます:
- geolocation, camera, microphone などの利用が、想定通りに制限され始める
- 開発・テスト環境では問題なかったが、本番でのみ Permissions-Policy を緩く解釈していたブラウザが、修正後は厳格に解釈するように見える場合がある
確認ポイント:
config.action_dispatch.permissions_policyなどで、Proc/ラムダを使ってソースを返している箇所を確認する:-> { :self }/-> { :none }/-> { :src }などキーワードを返していないか- 返り値が従来から想定していた文字列表現(
'self','none','src'など)になることを前提に書かれていないか
- ブラウザの DevTools で実際に送信されている
Permissions-Policy/Permissions-Policy-Report-Onlyヘッダを確認し、値が'self'など適切にクォートされていること、およびポリシーが想定通りに働いていることをチェックする。
- 参考情報 (あれば)
- この修正は、Content Security Policy における source mapping の挙動と整合を取る意図が明示されています。CSP での
:self→'self'のような扱いに慣れている場合、そのまま同じ感覚で Permissions-Policy の動的ソースも書けるようになります。 - 実際のコード差分は小さく(+16/-1)、
PermissionsPolicyクラス内のソース変換ロジックに対する一点修正と、それを保証するテスト追加が中心です。
#57938 Allow a trailing slash URL with a blank path to take query params
マージ日: 2026/7/1 | 作成者: @55728
- 概要 (1-2文で)
url_forでtrailing_slash: trueかつpath: ""(空文字)を指定した場合に、クエリパラメータやアンカーを付与するとFrozenErrorが発生していた不具合を修正した PR です。
これにより、"/"をベースに"/?a=1"のように正常にパラメータやアンカーを付与できるようになりました。
- 変更内容の詳細
問題の挙動
従来、以下のようなコードを実行すると:
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })- ベースのパスは
"path: "" + trailing_slash: true"により"/"になる - その後、この
"/"文字列に対してクエリパラメータ"?a=1"を「破壊的に」連結しようとする - しかし、内部的に使っている文字列が
freezeされたものだったため、破壊的変更ができずFrozenError: can't modify frozen String: "/"が発生
という状態になっていました。
修正内容
変更ファイル:
actionpack/lib/action_dispatch/http/url.rb(+1/-1)actionpack/test/dispatch/request_test.rb(+6)
url_for 内部の URL 生成ロジック(ActionDispatch::Http::URL 内)で:
trailing_slash: trueかつpathが空のケースで使われるパス文字列に対して
クエリパラメータやアンカーを付与する際に凍結された文字列を直接破壊的変更しないように修正
実装上は、凍結された文字列をそのまま << などで変更するのではなく、
必要に応じて複製(dup)した上でクエリやアンカーを連結する形に変えたと考えられます(+1/-1 なので単純な修正)。
サンプルコード
修正前
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => FrozenError: can't modify frozen String: "/"修正後
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => "/?a=1"
url_for(only_path: true, trailing_slash: true, path: "", anchor: "top")
# => "/#top"
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 }, anchor: "top")
# => "/?a=1#top"テスト (actionpack/test/dispatch/request_test.rb) では、
上記のようなケースが追加されているはずで、FrozenError が出ないことと、
期待する文字列が得られることを確認しています。
- 影響範囲・注意点
影響範囲
url_for(およびそれをラップしている*_url/*_pathヘルパなど)でtrailing_slash: truepathを空文字列("")または実質的にブランクと解釈されるケース- クエリパラメータ (
params) またはアンカー (anchor) を付与
という組み合わせを利用しているコードが対象になります。
- これまではこの組み合わせが事実上「使用不能」だった(例外が出ていた)ため、
既存アプリケーションがこの挙動に依存している可能性は極めて低く、
変更は基本的に後方互換的で安全と考えられます。
注意点
- 今回の修正により、
"/"をベースとするパスでもクエリ・アンカーを問題なく付与できるようになるので、
「ルートパスに対してパラメータを付けたい」ケースで、path: ""+trailing_slash: trueを
安心して使えるようになります。 - 内部実装で文字列の
dupなどが行われている場合、微小なオブジェクト生成コストは増えますが、
実用上は無視できるレベルと考えてよいです。
- 今回の修正により、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57938
- 該当コード周辺:
ActionDispatch::Http::URL(actionpack/lib/action_dispatch/http/url.rb) - 関連するヘルパー:
url_for, ルーティングヘルパー(root_path,root_urlなど)
#57951 Improve usage of Ractor.shareable_lambda
マージ日: 2026/7/1 | 作成者: @andrewn617
- 概要 (1-2文で)
Action Mailbox 内でRactor.shareable_lambdaの使い方を、&->を噛ませる形からブロックを直接渡す形に書き換えた、スタイル/可読性改善のための小さなリファクタリングです。挙動変更や機能追加はなく、既存の動作を保ったまま Ruby らしい書き方に揃えています。
- 変更内容の詳細(あればサンプルコードも含めて)
PRのモチベーションとして、以下のような「悪い例」が既にコードベースに入り込んでしまっていたので、今後は推奨しない書き方に揃えたい、という説明がなされています。
# bad
shareable_lambda(&->(foo) { foo })
# good
shareable_lambda { |foo| foo }Ractor.shareable_lambda は、Ractor(並列実行)間で共有可能なラムダを生成するためのメソッドで、actionmailbox のコールバック定義部分で使用されています。
この PR では actionmailbox/lib/action_mailbox/callbacks.rb 内の Ractor.shareable_lambda 呼び出しが、次のように変更されたと考えられます(実際のコードイメージ):
# 変更前(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda(
&->(inbound_email) { process_inbound_email(inbound_email) }
)
# 変更後(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda do |inbound_email|
process_inbound_email(inbound_email)
end技術的な意味としては:
&->(foo) { foo }は「ラムダを作ってから&でブロックに変換して渡す」二段構えshareable_lambda { |foo| foo }は「最初からブロックとして渡す」シンプルな形
であり、どちらも結果として shareable_lambda にブロックが渡され、そこから shareable な Proc(ラムダ)が生成される、という点では挙動は同じです。
この PR は、無駄な Proc 生成+ブロック変換をやめて、素直にブロックを渡すスタイルに統一する、というリファクタリングになります。
- 影響範囲・注意点
機能面の影響
- コールバックの挙動や Action Mailbox の外部 API は変わりません。
- ラムダの引数・戻り値の扱いも変わらないため、利用側のコード修正は不要です。
パフォーマンス
&->を経由しないため、わずかにオブジェクト生成が減る可能性はありますが、体感できるほどの差はほぼ期待できません。- 目的は最適化というより、スタイルと分かりやすさの改善です。
Ractor / スレッド安全性
Ractor.shareable_lambdaの使い方自体は変わらないため、Ractor 間共有の安全性に関する挙動も変わりません。
注意点
- 今後同様のコードを書く際は、
shareable_lambda(&-> { ... })ではなく、ブロックを直接渡す形で統一すると、コードベースと一貫しやすくなります。- 例:
Ractor.shareable_lambda { |mail| handle(mail) }
- 例:
- 今後同様のコードを書く際は、
- 参考情報 (あれば)
Ractor.shareable_lambdaについて- CRuby の Ractor API の一つで、「Ractor 間で共有しても安全なラムダ(Proc)」を生成するためのメソッドです。
- ブロック内で閉じ込めるオブジェクトは「共有可能 (shareable)」である必要があります(イミュータブルなオブジェクトや、
.freeze済みのオブジェクトなど)。
- コーディングスタイル的なポイント
- Ruby では通常のメソッド呼び出しに対し、
&-> {}で Proc を渡すよりもブロックをそのまま渡す方が標準的です。 - この PR は Rails コードベースのスタイルをそれに合わせる意図があります。
- Ruby では通常のメソッド呼び出しに対し、
#57952 Make parameter parsers assignment ractor safe
マージ日: 2026/7/1 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のパラメータパーサー (parameter_parsers) を設定する処理が Ractor セーフになるように、代入時に渡されたハッシュをfreezeするよう変更した PRです。あわせて、デフォルトパーサーの設定にパブリック API を経由せず、直接インスタンス変数に代入するようにして、不要なハッシュの複製を避けています。
- 変更内容の詳細(あればサンプルコードも含めて)
2.1 parameters_parsers 代入時の Hash を freeze
ActionDispatch::Http::Parameters(ActionDispatch::Request を通して利用される)で、parameters_parsers= のようなメソッドを使ってパラメータパーサーを設定する際、渡されたハッシュをそのまま内部で変形していたため、Ractor セーフではありませんでした。
この PR では、「パラメータパーサーを代入するタイミングで、そのハッシュを freeze してからインスタンス変数に設定する」ようにしています。
イメージとしては、以下のような変更です(実際のコードは数行レベルですが、概念的なサンプル):
# 変更前(イメージ)
def parameter_parsers=(parsers)
@parameter_parsers = parsers
# @parameter_parsers を内部で mutate する可能性がある
end
# 変更後(イメージ)
def parameter_parsers=(parsers)
@parameter_parsers = parsers.freeze
endこれにより、
ActionDispatch::Request.parameter_parsers = {
Mime[:json].symbol => ->(raw) { ... }
}のように設定すると、そのハッシュは freeze され、Rails 内部で意図せず書き換えられず、かつ Ractor から参照しても安全になります。
2.2 デフォルトパーサー設定でパブリック API を経由しない
以前のコミット(f2be45aaa8...)で、デフォルトのパラメータパーサー自体は Ractor セーフにしていたものの、そのデフォルト値を設定する際に、一度パブリックな setter を通していたため、同じハッシュの内容を二重に持つ(複製される)ような構造になっていました。
この PR では、「デフォルトパーサーを設定するときは、パブリック API (parameter_parsers= 等) を通さず、直接インスタンス変数(たとえば @parameter_parsers)に代入する」ように変更しています。
デフォルト値については Rails 側で形(キーの形式など)を保証できるため、わざわざ public API を通して整形/複製する必要がない、という判断です。
概念的には以下のイメージです:
# 変更前(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze
def initialize
self.parameter_parsers = DEFAULT_PARAMETER_PARSERS
end
# 変更後(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze
def initialize
@parameter_parsers = DEFAULT_PARAMETER_PARSERS
endこの変更により、
- デフォルトハッシュの無駄な複製を避けられる
- Ractor セーフ性を担保しやすくなる(共有オブジェクトの構造が単純になる)
という効果があります。
2.3 Proc の扱いについての考え方
PR の説明にもある通り、parameter_parsers の値として入るのは Proc(-> で定義されるラムダなど)であり、これらは call されるものです。
- Proc オブジェクトを Rails 側が勝手に
Ractor.make_shareableすると、ユーザが定義したクロージャに対する制約や予期せぬエラーを招く可能性がある - そのため、Proc の「shareable 化」までは Rails は行わず、「ハッシュそのものを freeze する」ことで、少なくともハッシュの構造変形による非 Ractor セーフ化を防ぐ、という方針です。
Ractor 越しに parameter_parsers を共有したい場合は、ユーザ側が適切に shareable な Proc を用意する/Ractor 内で定義する必要があります。
影響範囲・注意点
parameter_parsers=に渡したハッシュがfreezeされるようになるこれまでは setter に渡したハッシュを後から変更しても動いていたコードが、今後は
FrozenErrorになる可能性があります。例えば、以下のようなコードは動かなくなります:
rubyparsers = { Mime[:json].symbol => ->(raw) { ... } } ActionDispatch::Request.parameter_parsers = parsers # 後から追加しようとする parsers[Mime[:xml].symbol] = ->(raw) { ... } # => FrozenError対策としては、設定用のハッシュを使い回して外部から mutate しない、または、都度新しいハッシュを作って代入するようにする必要があります。
Ractor を利用するアプリでの安全性向上
- Ractor を使うマルチスレッド/マルチ Ractor 環境で
parameter_parsersを共有する際に、ハッシュがミューテートされないことが保証されるため、競合状態や「shared object ではないものを Ractor 間で共有した」という類のエラーを避けやすくなります。 - ただし前述の通り、「Proc 自体を shareable にする責任はユーザ側」にあるため、Ractor 間で共有したい場合はそこに留意が必要です。
- Ractor を使うマルチスレッド/マルチ Ractor 環境で
パブリック API を通さずにデフォルトを設定することによる外部 API への影響はほぼなし
- デフォルトのパラメータパーサーは内部的な構造が少しシンプルになっただけで、挙動(JSON 等のパース結果)は変わりません。
ActionDispatch::Request.parameter_parsersの getter/setter といったパブリック API 自体のインターフェースは変わっていないため、通常の利用コードには影響しません。
テスト・CHANGELOG について
- この PR 単体ではテストや CHANGELOG の更新は行われていません(チェックリスト上も未チェック)。
- 変更がかなり小さい(2行追加・2行削除)こと、および既存のテストでカバーされる範囲と判断されている可能性があります。
- 参考情報 (あれば)
この PR で言及されている既存コミット(デフォルトパラメータパーサーを Ractor セーフ化したもの):
https://github.com/rails/rails/commit/f2be45aaa8447b0d6efbcb7b42e5c6dbeb6b30b6関連クラス:
ActionDispatch::Http::ParametersActionDispatch::Request(parameter_parsersを通じて HTTP リクエストパラメータのパースを行う)
Ractor を積極的に使う Rails アプリで、ActionDispatch::Request.parameter_parsers をカスタマイズしている場合は、設定用ハッシュが凍結されることと、Ractor 越しに共有する Proc の shareable 性を意識しておくと安全です。
#57955 Ractor safe counter cache configuration
マージ日: 2026/7/1 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveRecord のカウンタキャッシュ設定情報(_counter_cache_columnsとcounter_cached_association_names)をデフォルトでfreezeし、Ractor(Ruby の並行オブジェクト実行モデル)でも安全に扱えるようにした PR です。これにより、Ractor 間で共有されるカウンタキャッシュ設定が不変オブジェクトとなり、スレッド/Ractor セーフティが向上します。
- 変更内容の詳細
何をしているか
ActiveRecord::CounterCacheが内部的に保持している以下の情報を「初期化時に frozen にする」よう変更しています。ActiveRecord::CounterCaches._counter_cache_columnsActiveRecord::CounterCaches.counter_cached_association_names
これらは主に、belongs_to :foo, counter_cache: true のような関連付けに対して、「どのカラムがカウンタキャッシュとして使われているか」「どの関連がカウンタキャッシュを持つか」というメタデータを保持するための構造です。
コードレベルでのイメージ
実際の差分は小さいですが、イメージとしては次のような変更が入っています(擬似コード):
# 変更前(イメージ)
module ActiveRecord
module CounterCaches
def _counter_cache_columns
@counter_cache_columns ||= {}
end
def counter_cached_association_names
@counter_cached_association_names ||= Set.new
end
end
end
# 変更後(イメージ)
module ActiveRecord
module CounterCaches
def _counter_cache_columns
@counter_cache_columns ||= {}.freeze
end
def counter_cached_association_names
@counter_cached_association_names ||= Set.new.freeze
end
end
endあるいは、belongs_to のビルダー側で設定を行うときに、
_counter_cache_columns[model_name] = somethingのような「後から破壊的変更」をせず、
「新しい frozen ハッシュ/セットを作って差し替える」形になっている可能性があります。
いずれにせよポイントは:
- 内部の設定オブジェクトを ミュータブルなまま共有しない
- Ractor 間で共有できるように freeze 済みオブジェクト として扱う
という設計変更です。
テストの追加
activerecord/test/cases/counter_cache_test.rb で、主に次のようなことを検証するテストが追加されていると考えられます:
ActiveRecord::CounterCaches._counter_cache_columnsが frozen であることActiveRecord::CounterCaches.counter_cached_association_namesが frozen であること- カウンタキャッシュ機能自体が、freeze された設定オブジェクトのもとでも従来どおり動作すること
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、「ActiveRecord のカウンタキャッシュ設定情報を内部 API として直接触っているコード」です。
- 通常の Rails アプリケーションで、以下のような典型的な使い方だけをしている場合は影響はほぼありません:rubyこのレベルであれば挙動は変わらず、カウンタキャッシュ機能はそのまま動作します。
class Comment < ApplicationRecord belongs_to :post, counter_cache: true end
注意点
内部構造への破壊的変更は不可になる
もしアプリや gem で、内部 API に直接触れて次のような操作をしている場合は、
FrozenErrorが発生する可能性があります:ruby# 悪い例(今後は FrozenError の可能性) ActiveRecord::CounterCaches._counter_cache_columns[:posts][:comments_count] = :some_value今後は、こうした「内部の Hash/Set を直接書き換える」前提のコードは動かなくなります。
Ractor を使う場合の安全性向上
- frozen 化により、これらの設定オブジェクトは Ractor 間で安全に共有できるようになります。
- Ractor を利用した並列実行をしている/今後検討している Rails アプリやライブラリでは、カウンタキャッシュ周りでのデータ競合・オブジェクト共有エラーのリスクが軽減されます。
カスタムパッチ・メタプログラミングに注意
- カウンタキャッシュ周りに独自の Monkey Patch やメタプログラミングを入れている場合、
- 「既存の Hash/Set を mutate するコード」
- 「freeze されていないことを前提にした処理」 がないかを確認する必要があります。
- カウンタキャッシュ周りに独自の Monkey Patch やメタプログラミングを入れている場合、
- 参考情報 (あれば)
- Ruby Ractor の仕様:
- Ractor 間で共有されるオブジェクトは、基本的に immutable(
freeze済み)である必要があるため、ライブラリ側では「設定・メタデータオブジェクトを初期化時に freeze しておく」パターンが一般的になりつつあります。
- Ractor 間で共有されるオブジェクトは、基本的に immutable(
- この PR は CHANGELOG 更新が行われていないため、Rails 側では「挙動変更というより内部実装の安全性向上」として扱われていると考えられますが、内部 API を触っているコードがある場合はアップデート時に注意が必要です。
#57922 Freeze the inflection rules after the app boots
マージ日: 2026/7/1 | 作成者: @Edouard-chin
- 概要 (1-2文で)
このPRは、Railsアプリケーションのブート完了後にActiveSupport::Inflectorの「変換ルール(inflection rules)」を変更できないようにし、すべての言語ごとの inflections を freeze する変更です。これにより、起動後の不定な挙動を防ぎつつ、将来的な Ractor 対応も見据えたスレッドセーフ性の向上を図っています。
- 変更内容の詳細
2-1. 何が変わったか
- 言語ごとに保持されている inflection インスタンス(単数・複数形変換、例外ルールなどを持つオブジェクト)を、アプリケーションブート完了時に
freezeするようになりました。 - freeze 後は、
ActiveSupport::Inflector.inflections経由でルールを追加・変更しようとすると、FrozenErrorが発生します。 - Rails のブートシーケンス(
ActiveSupport::Railtie)に、inflections を freeze するフックが追加されています。 - これに対応するテストが
railties/test/application/active_support_railtie_test.rbに追加されています。
2-2. 想定される処理の流れ
典型的な Rails アプリでは、config/initializers/inflections.rb などで起動時に inflection ルールを定義します。
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.irregular 'person', 'people'
inflect.acronym 'API'
endこの PR 適用後も、ブート前(初期化時)のこのコードは有効です。
アプリケーションがブートし終わると:
ActiveSupport::Inflector.inflections(:en).frozen? #=> trueとなり、以降は inflections を変更しようとするとエラーになります:
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.irregular 'mouse', 'mice'
end
# => FrozenError (can't modify frozen ActiveSupport::Inflector::Inflections)2-3. コードレベルのポイント (推測を含まない範囲で)
変更ファイルから読み取れる論点(PRの説明とdiffの規模から確実に言える範囲):
activesupport/lib/active_support/inflector/inflections.rb- Inflections インスタンスに対し、後から書き換えられないようにするための freeze ロジックを追加。
- 「言語ごとに1つ存在する inflection インスタンス」をまとめて凍結する仕組みが導入された可能性が高い(説明文より)。
activesupport/lib/active_support/railtie.rb- Rails アプリケーションブート完了時(
on_loadまたは初期化プロセスの終盤)に、全 inflection インスタンスを freeze するフックが追加。
- Rails アプリケーションブート完了時(
railties/test/application/active_support_railtie_test.rb- ブート後に inflections を変更できないことを確認するテストケースが追加。
- おそらく「アプリ起動後に inflections を変更しようとして FrozenError が起きる」ことや、「ブート時に定義したルールは正しく適用される」ことを検証。
- 影響範囲・注意点
3-1. 影響を受けるケース
以下のようなコードを書いていると、この変更で動かなくなる/エラーが出る可能性があります。
アプリケーションブート後に inflections を変更している場合
例:
- リクエストごとに inflection を変える
- マルチテナントごとに inflection を動的に追加する
- コンソール起動後に initializer 相当の処理を実行して inflection を追加している
ruby# 例: Rails 起動後 / リクエスト中に ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'JSONAPI' end # => FrozenError 発生これらは、アプリケーションブート前(initializer)に移動する必要があります。
gem やエンジンが遅いタイミングで inflections をいじる場合
- エンジンやライブラリが
to_prepareやミドルウェア・リクエスト処理の中でActiveSupport::Inflector.inflectionsを変更している場合、これも FrozenError の対象となりえます。 - gem 開発者は、inflection 変更を Rails の初期化プロセス中に移動し、ブート後に変更しない設計にする必要があります。
- エンジンやライブラリが
3-2. なぜこの仕様変更が妥当か
不定なコードパスの排除
起動後に inflection が変わると、例えば以下がリクエスト順序やタイミングによって変化する可能性がありました。- モデル名 → テーブル名 のマッピング
- ルーティングのパス名
- 他、inflector を用いる各種内部処理
これは「あるタイミング以降でだけ挙動が変わる」ため、バグの再現や原因特定が非常に難しくなります。本PRはこれを防ぎます。
Ractor セーフティの向上
Ruby 3 の Ractor を考えると、「起動後に共有オブジェクトを変更可能であること」は競合やメモリ可視性の問題を生みます。
inflections を freeze してイミュータブルにすることで、「起動前に構築 → 起動後は読み取り専用」という Ractor 向きのモデルになります。
3-3. マイグレーション指針
アプリ開発者:
- inflection の定義や変更は
config/initializers/inflections.rbなどのブート前処理に集約する。 - 動的に inflection を変える設計は避け、必要なら自前ロジック(独自の変換テーブルやサービスクラス)を用意する。
- inflection の定義や変更は
gem / エンジン開発者:
- Railtie / Engine の
initializerブロック内で inflections を設定し、to_prepareや実行時に inflections を再定義しない。 - どうしても追加設定のタイミングがブート後になるなら、inflector を使わない別アプローチを検討する。
- Railtie / Engine の
- 参考情報 (あれば)
関連概念:
ActiveSupport::Inflectorと inflections:ActiveSupport::Inflector.inflections(:en)で取得できるオブジェクトに対し、plural,singular,irregular,uncountable,acronymなどでルールを定義する仕組み。- Ractor セーフティ:
共有ミュータブル状態をなくし、起動前に構築したイミュータブルデータを並行に読む構造にすることで、マルチ Ractor 実行時の安全性を高める。
実運用のベストプラクティス:
- 以前からも「inflection の定義は initializer で行い、実行時に変更しない」ことが暗黙の前提/推奨だったため、その暗黙のルールを仕様として厳密に enforce した形の変更と捉えると理解しやすいです。
#57823 Initialize ActionView::LookupContext.view_context_class early
マージ日: 2026/7/1 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::LookupContext.view_context_classをできるだけ早いタイミングで初期化することで、enable_reloading = falseな環境ではミューテックスに触らずに済むようにし、Ractor からも追加セットアップなしで安全にビューコンテキストクラスを利用できるようにした変更です。
- 変更内容の詳細
※PR本文とファイル差分の要約ベースでの解説です(実際のコードは概念的なイメージ)。
2-1. LookupContext での view_context_class 初期化タイミングの変更
actionview/lib/action_view/lookup_context.rb において:
これまで:
view_context_classは必要になったタイミングで決定 / キャッシュされ、その際にミューテックスを用いた同期が行われていた。- そのため、スレッド/Ractor の利用状況によっては、
view_context_classの決定時にロック処理が発生していた。
変更後:
LookupContextの初期化時、もしくは「リロードを必要としない (enable_reloading = false)」ことが分かっている場合に、あらかじめview_context_classを決定しておくようなロジックに変更。enable_reloading = falseの場合は、ビューコンテキストクラスが後から変化しない前提のため、一度決定してしまえば再度ミューテックスを取る必要がない。
その結果、enable_reloading = false の環境では:
view_context_classの取得がロックフリー(またはほぼロックフリー)になる。Ractorからも「既に決まっている不変のクラス定義」としてview_context_classをそのまま利用できる。
2-2. ActionView::Rendering 側の参照調整
actionview/lib/action_view/rendering.rb にも 1 行の変更があり、これは:
view_context_classの取得方法やタイミングを、LookupContextの新しい初期化タイミングに合わせて調整したものと考えられます。- 実質的には「ビューコンテキストを生成する際に、すでに初期化済みの
view_context_classを使う」の一貫性を保つ変更です。
2-3. テスト追加
actionview/test/template/lookup_context_test.rb にテストが 16 行追加されています。
テストの狙いとしては:
enable_reloading = falseのときにview_context_classが早期にセットされ、以降ミューテックスを介さずに参照できること。 -(間接的に)Ractor からも同じクラスを利用できる、もしくは並行実行時に問題が起きないこと。
といった挙動を担保していると考えられます。
- 影響範囲・注意点
3-1. パフォーマンスと並行性
- パフォーマンス改善:
enable_reloading = false(本番環境が多い)で、view_context_class取得時のミューテックス獲得が避けられるため、ビューのレンダリングにおけるロックコストが削減されます。
- Ractor 対応の強化:
- Ractor 内から
ActionViewを使う場合でも、追加のセットアップなしでview_context_classを共有しやすくなっています。 - Ractor では「オブジェクトがイミュータブル or Ractor シェア可能」である必要がありますが、
enable_reloading = falseなら「クラス定義が途中で差し替わらない」ため、Ractor から扱いやすいモデルになります。
- Ractor 内から
3-2. enable_reloading = true (開発環境など)の挙動
- 開発環境では通常
enable_reloading = trueのため:- 従来どおり、クラスの再定義やテンプレートの変更に対応する必要があり、必要な箇所ではミューテックスを使った同期が残ります。
- この PR の主な恩恵(ロック回避・Ractor での利用容易化)は主に
enable_reloading = false側で現れます。
3-3. アプリ側コードへの影響
- アプリコードで
ActionView::LookupContext.view_context_classを直接触っていない限り、既存アプリケーションの動作は基本的に変わらない想定です。 - もし以下のようなことをしている場合は注意が必要です:
view_context_classをモンキーパッチで動的にすげ替えている- 初回アクセス時に何か副作用を期待している
- 今回は「初回決定タイミングがより早くなった」という性質の変更なので、
view_context_class決定時の副作用に依存しているコードがあると、タイミングずれの可能性があります(通常はそのような依存は推奨されません)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57823
ActionView::LookupContextドキュメント:
https://api.rubyonrails.org/classes/ActionView/LookupContext.html- Ractor について(Ruby 3 以降の並行性モデル):
https://docs.ruby-lang.org/en/master/doc/ractor_md.html
この変更は「本番運用でのロック削減」と「Rails + Ractor の相性改善」の両方に関連するため、Rails を Ruby 3 系で Ractor 活用したり、高負荷環境でのビューレンダリング性能に関心がある場合に重要なアップデートになります。
#57936 Deduplicate columns in default_order
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
default_orderに同じカラムを複数回渡した場合、ORDER BY句に重複して出力されないよう重複排除される変更です。これにより、すでに同様の挙動をしていたorderやregroupと一貫した動きになります。
- 変更内容の詳細
挙動の変更点
これまで:
User.default_order(:name, :name)
# ORDER BY "users"."name" ASC, "users"."name" ASCこの PR 適用後:
User.default_order(:name, :name)
# ORDER BY "users"."name" ASCdefault_order に渡した引数リストから、同じカラム指定を重複して使わないように、内部的に「重複カラムの除去(デデュプリケーション)」を行うようにしています。
実装上のポイント
- 変更ファイル:
activerecord/lib/active_record/relation/query_methods.rbdefault_orderで使用するカラムリストに対して、orderやregroup同様に重複カラムをまとめる処理を追加(1行)。
- テスト:
activerecord/test/cases/relations_test.rbdefault_order(:name, :name)のようなケースで、生成される SQL のORDER BY句に同じカラムが 1 回だけ現れることを確認するテストが追加(5行)。
実際には、既存の order / regroup の実装と同じような重複排除ロジック(例: uniq 的な処理)を default_order のカラム処理にも適用した形と考えられます。
- 影響範囲・注意点
主な影響範囲
default_orderを使っており、同じカラムを意図的/偶発的に複数回指定しているコードで、生成される SQL が変わります。例えば、scope やモデルレベルで
default_orderを定義しているコード:rubyclass User < ApplicationRecord default_scope { default_order(:name, :name, :created_at) } endこれまでは
nameが複数回出ていたのが 1 回にまとまります。
パフォーマンス・動作への影響
ORDER BY句の重複列が論理的な結果に違いを生むことは基本的にありません(同じ列・同じ方向での並び替えは冗長でしかないため)。- 従って、アプリケーションの機能的挙動が変わる可能性はほぼなく、SQL 文字列の見た目が変わる程度の影響にとどまります。
- 一部、生成される SQL 文字列そのものを文字列比較でテストしている場合は、テストが落ちる可能性があります。その場合は、新しい形式(重複のない
ORDER BY)に合わせて期待値を更新する必要があります。
一貫性の向上
- 既に
orderとregroupは同種の重複排除をしているため、default_orderだけが異なる挙動をしていた状態が解消されます。 - これにより、開発者は「ActiveRecord の並び替え関連メソッドは重複カラムを自動的にまとめる」という前提でコードを書けるようになります。
- 既に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57936
- 関連するメソッド:
ActiveRecord::QueryMethods#orderActiveRecord::QueryMethods#regroupActiveRecord::QueryMethods#default_order(この PR の対象)
もし既存コードで default_order を多用している場合は、テストスイートを走らせて SQL 文字列比較テストの影響有無を確認する程度で十分です。
#57856 Load ActionDispatch::Request::Session and Utils at boot time
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1–2文で)
ActionDispatch::Request::Sessionとその内部で使われるUtilsを「初回リクエスト時のオートロード」から「アプリ起動時のロード」に変更するためのリファクタリングです。- これにより ractor 利用時など並行環境でのオートロード問題や循環 require を避けつつ、
SessionをActionDispatch::Http::Sessionに移動して責務の位置付けを整理しています。
- 変更内容の詳細
背景
- 以前の変更(rails/rails@8ce6b0c)で、
ActionDispatch::Request::Sessionは循環 require を避けるために autoload されるようになっていました。 - しかし autoload は「初回アクセス時に読み込む」ため、Ractor などスレッド/並行実行環境では「実行中にクラス定義が変わる」状態を招きやすく、問題になります。
- この PR では、セッション関連クラスとユーティリティを「HTTP レイヤのクラス」として再配置し、
ActionDispatch::Request読み込み時点で一緒に require されるように変更しています。
主なリファクタリングポイント
1) ActionDispatch::Request::Session → ActionDispatch::Http::Session への移動
- 旧構成(イメージ):
# action_dispatch/http/request.rb
module ActionDispatch
class Request
class Session
# セッション関連のクラス定義
end
end
end- 新構成(イメージ):
# action_dispatch/http/session.rb
module ActionDispatch
module Http
class Session
# セッション関連のクラス定義
end
end
endRequest::Sessionはドキュメント化されていない内部 API という扱いであり、クラスの場所を変えても外部 API 互換性の問題は小さい、という判断になっています。ActionDispatch::RequestはActionDispatch::Http::Sessionに依存する側として整理され、循環参照を避ける構造になっています。
2) Utils を HTTP 層に移動し、起動時に require
UtilsもRequest::Sessionと同様に HTTP レイヤで使われる内部ユーティリティとして扱い、action_dispatch/http/utils.rbに整理。ActionDispatch::Httpに関連するファイル (request.rb,parameters.rb,param_builder.rbなど) から、Utilsを明示的に require する形に変更し、起動時にロードされます。
# actionpack/lib/action_dispatch.rb
require "action_dispatch/http/session"
require "action_dispatch/http/utils"
# など Http 関連の require が追加・整理3) autoload / require の整理
action_dispatch.rb,action_dispatch/http/request.rb,action_controller/metal.rb,action_dispatch/railtie.rbなどで、SessionとUtilsの読み込み経路が調整されています。- これまで「最初のリクエスト時に autoload」されていたものが、
ActionDispatch/Http読み込みタイミングでrequireされるように変更。
- テスト (
request_test.rb,session_test.rb他) も、新しいクラス名・モジュール構成に合わせて参照先を更新しています。
4) 仕様追加・挙動変更はなし
- 差分は主に「クラス/モジュールの配置 と ロードタイミング」であり、セッション機能自体の振る舞い(API やストアの挙動)は変えていません。
- テストの追加・変更も挙動変更ではなく、クラスパス変更に追随するための修正が中心です。
- 影響範囲・注意点
影響範囲
内部 API (
ActionDispatch::Request::Session) に依存しているコード- ドキュメント外 API ですが、もしアプリやライブラリが
ActionDispatch::Request::Sessionを直接参照している場合、今回の変更により壊れる可能性があります。 - 新しい正しい参照先は
ActionDispatch::Http::Sessionになります。
- ドキュメント外 API ですが、もしアプリやライブラリが
初期化時のロードコスト
- これまで「初回アクセス時にオートロード」されていた
Session/Utilsが「ブート時にロード」されるため、起動時間がわずかに増える可能性があります。 - ただし、ActionDispatch 周りの他のクラスもすでにブート時にロードされており、相対的なインパクトは小さいと考えられます。
- これまで「初回アクセス時にオートロード」されていた
Ractor / 並行実行環境での安定性向上
- Rails プロセス起動後、アプリケーションコード実行中に
Sessionクラスが新たに定義されることがなくなるため、Ractor やマルチスレッド環境でのオートロード競合・定義の途中読み取りなどのリスクが低減します。 - ractor 対応や高並行処理を視野に入れている場合はプラスの変更です。
- Rails プロセス起動後、アプリケーションコード実行中に
注意点・確認ポイント
- 自前で
autoloadしている箇所やconst_get("ActionDispatch::Request::Session")のようなメタプログラミングをしている場合、クラスの場所変更により動作が変わるので確認が必要です。 ActionDispatch::Http::Sessionを直接使うつもりがない通常の Rails アプリ(コントローラレベルでしかセッションを扱わないケース)では、ほぼ影響はありません。- 将来的に Ractor を有効にする、あるいは Zeitwerk の strict モード・autoload 制限を強めたい場合には、この変更がその下準備の一つになっています。
- 参考情報 (あれば)
- この PR の直接の動機: Ractor 対応作業中に「
ActionDispatch::Request::Sessionが初回リクエストまでロードされない」=並行環境で問題になり得る autoload パターンであることが発覚したため。 - 関連コミット:
rails/rails@8ce6b0c- ここで
ActionDispatch::Request::Sessionが autoload 化され、循環 require 回避の暫定措置が導入されていた。
- ここで
- 設計上の意味:
- 「HTTP 関連の低レベルクラス (
ActionDispatch::Http::Session,ActionDispatch::Http::Utils)」と - 「それを利用する高レベルな
ActionDispatch::Request」 - という依存の向きが明確化され、Rails のロード順や並行実行時の安全性の面で一貫した構造になっています。
- 「HTTP 関連の低レベルクラス (
#57684 Add a Ractor-shareable ActiveSupport::TaggedLogging.shareable_logger
マージ日: 2026/6/30 | 作成者: @etiennebarrie
- 概要 (1–2文で)
Ractor から安全に共有できるログ出力用 APIActiveSupport::TaggedLogging.shareable_loggerが追加され、複数 Ractor / スレッドから同じ logger を使える仕組みが導入されました。内部的にはバックグラウンドスレッドに書き込み処理を委譲するActiveSupport::ShareableLoggerを tagged logging でラップする構成です。
- 変更内容の詳細
新しく追加された概念とクラス
2-1. ActiveSupport::TaggedLogging.shareable_logger
従来の:
logger = ActiveSupport::TaggedLogging.logger(STDOUT)に対応する形で、Ractor 共有可能な logger を返すクラスメソッドが追加されています:
logger = ActiveSupport::TaggedLogging.shareable_logger(STDOUT)
# => ActiveSupport::TaggedLogging でラップされた ShareableLogger特徴:
- 返される logger は
ActiveSupport::ShareableLogger(::Loggerのサブクラス)を内包 - Ractor 間で共有可能になるよう設計されており、複数 Ractor から同じ logger に書ける
- TaggedLogging によるタグ付きログ (
tagged("request-id") { ... }) が通常通り使える
2-2. ActiveSupport::ShareableLogger の構成
コード的には以下のような構図になっています(ファイルは activesupport/lib/active_support/ractors/logger/*.rb):
Writerクラス- バックグラウンドで、実際の
logdev(IO / ファイルなど)に書き込む専用スレッド - 他の Ractor/スレッドから送られてきたメッセージをキューから取り出して順次書き込む
- 書き込み中に例外が起きた場合は:
- 元の logger と同様に
stderrにフォールバックして出力 - 呼び出し側には例外を伝播させない(ログ出力失敗がアプリの例外にならない)
- 元の logger と同様に
- バックグラウンドで、実際の
DeviceProxyクラスLogger::LogDeviceを直接触らず、その前段で「書き込み要求をキューに積む」役割write,close,reopenなどの操作を、Writer スレッドにメッセージとして渡す- これにより、アプリ側スレッド/Ractorは実ログデバイスに直接触らない
概念図:
[Ractor A] --\
[Ractor B] --- logger.info(...) -> DeviceProxy -> [queue] -> Writer thread -> logdev
[Ractor C] --/2-3. エラーハンドリング
説明文にある通り、ログ書き込みエラー時の挙動は既存 Logger と同等になるように設計されています。
- 書き込み中に IO エラーなどが発生
- Writer 側でエラーを検知
stderrへの書き出しにフォールバック- 呼び出し元 (
logger.infoしたコード) には例外を返さない
ActiveSupport::ErrorReporter を使う案も検討されたが、それ自体が logger にフォールバックする設計のため、循環参照的になって断念、という経緯が説明されています。
2-4. Ractor 対応のためのインフラ
activesupport/lib/active_support/ractors.rb に Ractor 関連のサポートコードが追加され、それを使って:
- Ractor 間で安全にやり取りできるオブジェクト/メッセージの制約
- Writer スレッドとの連携
などを行う仕組みが提供されています。
2-5. テスト
activesupport/test/ractor_logger_test.rb(約 260 行)が追加- 複数 Ractor から同一 logger に書き込んでも正しくシリアライズされるか
- flush の挙動
- エラー時の挙動(stderr へのフォールバックなど)
railties/test/application/configuration_test.rb- Rails アプリケーション設定との統合(config 経由での利用など)が壊れていないことの確認
- 影響範囲・注意点
3-1. 利用シナリオ(想定)
- Ractor ベースの並列化を行う Rails / ActiveSupport アプリケーションで、単一の logger を全 Ractor から安全に使いたいケース
- 通常のスレッドベースの Rails アプリでも「非同期ログ書き込み」を使ってみたいケース
- PR 説明では「通常の threaded Rails アプリでも使えるが、パフォーマンス向上があるかは不明」と明記
3-2. キューの無制限成長リスク
- 書き込み用キューに上限がないため、ログが大量に出るエンドポイントで「Producer(アプリ側)が早く、Writer が追いつかない」状態になると、キューが肥大化しメモリを圧迫しうる
- 緩和策:
- 「リクエストの終わりで logger を flush する」という契約を前提としている
- 例: Rack ミドルウェアや around_action で
logger.flush相当を呼ぶ設計を想定
- 例: Rack ミドルウェアや around_action で
- 「リクエストの終わりで logger を flush する」という契約を前提としている
- 作者は別ブランチで「一定しきい値を超えたら同期書き込みに切り替える」backpressure 付き実装を用意しているが:
ractor_safegem への依存が必要- そのため Rails 本体には入れず、オプション扱いに留めている
3-3. 互換性と今後の改善余地
- 現段階では「ドキュメント未掲載の内部的/実験的 API」という位置付け
- 公開メソッドだが、将来的な API 変更の可能性がある
- 将来的な効果として期待されているもの:
Logger::LogDevice内の mutex を無効化できる可能性(ログデバイスへの書き込みを Writer スレッドに集約するため)- これにより、マルチスレッド環境での logger 周りのロック競合を減らせるかもしれない
3-4. 導入時の実務的な注意点
- Ractor を本格利用していない通常の Rails アプリで導入する場合:
- 「本番での安定性・メモリ使用量」を慎重に観測しながらロールアウトするのが良さそう
- ログ量が多いバッチ処理・エンドポイントでは特に、flush のタイミングとキューのサイズに注意
- Ractor を使う場合:
- logger を Ractor に渡す前に、Ractor-shareable であることを確認する設計にする
- Ractor 内で logger の再構築を繰り返さず、極力共有して利用するほうがパフォーマンス上有利
- 参考情報 (あれば)
- この PR の説明内で参照されている follow-up ブランチ(backpressure 付き実装案)
- 関連するクラス/ファイル
ActiveSupport::TaggedLoggingshareable_loggerが追加され、Ractor 対応 logger を生成
activesupport/lib/active_support/ractors/logger/writer.rb- バックグラウンド Writer スレッド本体
activesupport/lib/active_support/ractors/logger/device_proxy.rb- Ractor から Writer への非同期ブリッジ
activesupport/test/ractor_logger_test.rb- 具体的な利用・挙動を把握するのに有用なテストケース群
#57937 Load database config when a connection is absent from the shared section
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
database.ymlでsharedセクションに存在しない接続名が環境ごとに定義されている場合、これまではNoMethodErrorで落ちていたのを、「その接続は shared マージ対象外として素通りさせる」ように修正した PR です。これにより、3階層構造のdatabase.ymlをより柔軟に書けるようになり、安全にロードできるようになりました。
- 変更内容の詳細
何が問題だったか
3階層の database.yml を以下のように書いたケースを考えます:
shared:
one:
migrations_path: "db/one"
development:
one:
adapter: sqlite3
two: # `shared`にtwoが無い
adapter: sqlite3Rails は Rails.application.config.database_configuration を構築する際に、
sharedセクションに同名のサブセクションがあれば、それを各環境の同名接続にマージする- 例:
shared.oneをdevelopment.oneにマージする
という処理を行っています。
しかし実装上、shared 側に 存在しない 接続名に対しても無条件に merge を呼んでおり、shared[:two] が nil なのに merge しようとして、
NoMethodError: undefined method `merge` for nilが発生していました。
このため、shared では一部の接続だけに共通設定を定義し、他の接続は shared 未定義のまま個別設定で使う、というごく自然な使い方ができませんでした。
どう直したか
railties/lib/rails/application/configuration.rb の shared マージ処理の中で、
- shared 側に該当のキーがあるときだけ
mergeする - shared 側にキーが無い場合は、その接続設定は何もせず「そのまま残す」
ように条件分岐を追加しています(実質 +1/-1 の小さな修正)。
疑似コードで表すと:
# 変更前(イメージ)
merged = env_config.merge(shared_config[name]) # shared_config[name] が nil でも merge する
# 変更後(イメージ)
if shared_config[name]
merged = env_config.merge(shared_config[name])
else
merged = env_config # sharedに無ければそのまま
endこれにより、次のような挙動になります:
Rails.application.config.database_configuration
# Before: NoMethodError: undefined method 'merge' for nil
# After: 正常にロードされ、"two" は shared のマージを受けずにそのまま残るテストの追加
railties/test/application/configuration_test.rb にテストが追加されており、
sharedに存在する接続 (one) には共通設定がマージされるsharedに存在しない接続 (two) は例外を出さず、そのままの設定で残る
ことを確認しています。行数的に見ると、上記の YAML 構造に近い形のテストケースが1つ追加された形です。
- 影響範囲・注意点
影響範囲
config/database.ymlで 3階層 (shared+ 各環境 + 接続名) 構造を使っているアプリケーションに影響します。- 特に、
sharedが「一部の接続だけ」を定義しているプロジェクトで、これまでNoMethodErrorに遭遇していたケースは、この修正で解消されます。 sharedにすべての接続名を定義していたプロジェクトでは挙動は変わりません。
互換性 / 注意点
- shared 側に無い接続名は、今後も shared の設定を一切マージされず「完全に独立した接続」として扱われます。
- もし「本当は shared にも書くつもりだったが、書き忘れていた」ような設定ミスは、この変更では検出されず、静かに素通りします。
- そのようなミスを検出したい場合は RuboCop などで独自ルールを追加するか、CI で
database.ymlの検証を自前で行う必要があります。
- Rails 本体としては「shared に存在しない接続はマージ対象にしない」という仕様を明確化したと言えます。
- shared 側に無い接続名は、今後も shared の設定を一切マージされず「完全に独立した接続」として扱われます。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57937
- 関連しそうなキーワード:
- 3-tier database.yml (
shared/development/ 接続名) - マルチ DB 構成 (
primary,replica,readonlyなど) Rails.application.config.database_configurationの解決ロジック
- 3-tier database.yml (
#57946 Allow the Evented LogSubscriber to be shareable
マージ日: 2026/6/30 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Evented な LogSubscriber(および関連する StructuredEventSubscriber)の実装から「Proc を含む定数ハッシュ」を排除し、Ractor でも共有可能(shareable)になるようにリファクタリングした PR です。Ractor.shareable_procを使わずに、設計側で Proc 依存を解消することで Ractor セーフティを高めています。
- 変更内容の詳細
※実際のコードを要約した概念的な説明です。
背景となる問題
- もともと
ActiveSupport::EventReporter::LogSubscriber(とそれに関連するActiveSupport::StructuredEventSubscriber)には、- 「イベント名 → 処理内容」を表す 定数ハッシュ が存在
- そのハッシュの値として Proc オブジェクト が格納されていた
- 定数自体は
freezeされていても、中に非 shareable なオブジェクト(Proc)があるため、Ractor 間で安全に共有できない状態だった。 Ractor.shareable_procでラップすれば shareable にできるが、- そのたびにラップするのが煩雑
- コードの可読性も下がる
という理由で、設計レベルで Proc を定数ハッシュから排除する方向にリファクタリングしている。
主なリファクタリング内容
変更ファイルは次の2つです。
activesupport/lib/active_support/event_reporter/log_subscriber.rb(+10/-6)activesupport/lib/active_support/structured_event_subscriber.rb(+2/-4)
おおまかな方向性は以下の通りです。
定数ハッシュから Proc を排除
以前は、例えばこういった形だったものを(※イメージです):rubyEVENT_PROCESSORS = { "sql.active_record" => ->(event) { process_sql_event(event) }, "render_template.action_view" => ->(event) { process_render_event(event) }, }.freezeこれを Proc を値に持たない形 に変更し、
- シンボルや識別子だけを格納する
- 実際の処理はメソッドディスパッチなどで行う
という設計に変えています。例えば次のような形に近づけている可能性が高いです:
rubyEVENT_PROCESSORS = { "sql.active_record" => :process_sql_event, "render_template.action_view" => :process_render_event, }.freeze def call(event) if (handler = EVENT_PROCESSORS[event.name]) send(handler, event) end endこうすることで、
EVENT_PROCESSORSは- 文字列/シンボルのみで構成される
- 完全に shareable なオブジェクト になり、Ractor 間で安全に共有できます。
Evented LogSubscriber のコードパスを Ractor 安全に
- Evented なログ購読ロジック(通知を受け取り、ログ出力や構造化イベントに変換する部分)が、
- Ractor 内から利用される
- かつグローバルに共有される可能性
を考慮して、「共有しても安全なオブジェクト(Ractor.shareable)」だけを定数として持つようになっています。
StructuredEventSubscriber側でも、同じ定数ハッシュや共通ロジックを参照している部分があるため、そこも Proc 非依存に合わせて微調整されています(+2/-4 程度の小修正)。
- Evented なログ購読ロジック(通知を受け取り、ログ出力や構造化イベントに変換する部分)が、
Ractor.shareable_procを使わない構成にRactor.shareable_procは、ブロックや Proc を Ractor 間共有可能にするための仕組みですが、- ラップが散在しがち
- 「どこまでが shareable でどこからが非 shareable か」が読み取りにくくなる
- この PR では、それを使う代わりに
- 「そもそも定数の中に Proc を入れない」
- 「Proc 的な振る舞いはインスタンスメソッドやクラスメソッドのディスパッチに寄せる」 方針で整理しています。
- 影響範囲・注意点
- 主な影響範囲
ActiveSupport::EventReporter::LogSubscriberActiveSupport::StructuredEventSubscriber
- これらを直接継承・拡張しているコードが、
- 定数ハッシュに Proc を追加・上書きしている
- 内部の実装に強く依存している 場合は、挙動の差異や互換性に注意が必要です。
- 公開 API としては、「イベント名に応じてログ/構造化イベントが処理される」という仕様自体は変わらない想定です。
- この PR 自体では テストは追加・更新されていない ため、
- Ractor を使う環境での Evented ログ出力
- StructuredEventSubscriber 経由のイベント処理
などを実際のアプリケーション・本番に近い環境で確認するのが無難です。
- 今後、Ractor を前提とした並行実行(例: 複数 Ractor から ActiveSupport のイベントを購読・ログ出力する)を行う際の 前提条件 として、この変更が効いてきます。
- 参考情報 (あれば)
- Ruby の Ractor と shareable オブジェクトの仕様:
- RDoc: https://docs.ruby-lang.org/en/master/Ractor.html
Ractor.shareable?,Ractor.make_shareable,Ractor.shareable_procなど
- Rails 内での関連コード:
ActiveSupport::LogSubscriberActiveSupport::Notificationsこれらと Evented LogSubscriber / StructuredEventSubscriber は密接に連携しているため、イベント処理まわりを拡張する場合はソースを一緒に読むと理解しやすいです。
#57816 Set browser binary when preloading system test driver
マージ日: 2026/6/30 | 作成者: @titusfortner
- 概要 (1-2文で)
Rails の system test で Selenium ドライバをプリロードする際に、Chrome/Firefox のブラウザ実行ファイルの場所(options.binary)が設定されず「cannot find Chrome binary」で落ちる問題を修正する PR です。
Selenium 側の仕様変更(DriverFinderがoptionsをミューテートしなくなった)に追従し、Rails 側で明示的にブラウザバイナリを設定するようにしています。
- 変更内容の詳細
背景
- Rails の system test で Selenium ドライバを「プリロード」する際、
action_dispatch/system_testing/browser.rbのBrowser#preloadが Selenium のChrome::Service.driver_path/Firefox::Service.driver_pathをグローバルに設定していました。 - 以前は Selenium の
DriverFinderが- ドライバのパス解決(
driver_path) - ブラウザ実行ファイルパスの設定(
options.binary) の両方を行っており、options.binaryを書き換える副作用に依存していました。
- ドライバのパス解決(
- Selenium 4.45 で
DriverFinderがoptionsをミューテートしなくなり、その副作用が消滅。- Rails 側では
Service.driver_pathだけが設定される - その結果、各 worker がドライバを作るとき Selenium Manager がスキップされ、「ドライバパスだけ判明し、ブラウザパスは未設定」の状態になる
options.binaryが空のままになり、「cannot find Chrome binary」で失敗する
- Rails 側では
この PR の対応
ポイントは「ドライバパスとブラウザパスを同じ DriverFinder から取得し、options.binary を Rails 側で明示的に設定する」ことです。
actionpack/lib/action_dispatch/system_testing/browser.rb の変更:
- これまで:
Browser#preload内でDriverFinderを呼び出してService.driver_pathを設定DriverFinderが内部で勝手にoptions.binaryも設定することに依存
- これから:
- 同じ
DriverFinder呼び出しで、ドライバパスとブラウザパスを両方取得 - 取得したブラウザパスを
options.binary = <resolved browser path>として明示的に代入
- 同じ
疑似コードイメージは以下のような形になります(実際の API 名は Selenium 実装に依存しますが、やっていることのイメージとして):
def preload
# options は system test 用の Selenium::WebDriver::Chrome::Options など
options = build_options
# DriverFinder からドライバとブラウザの両方を解決
driver_finder = Selenium::WebDriver::DriverFinder.new(browser: :chrome, options: options)
driver_path = driver_finder.driver_path
browser_path = driver_finder.browser_path # ここからブラウザパスも取得(新しく明示的に利用)
# ドライバパスをグローバル Service に設定(従来どおり)
Selenium::WebDriver::Chrome::Service.driver_path = driver_path
# ブラウザバイナリを明示的に設定(新しく追加された部分)
options.binary = browser_path
# …以降、プリロード完了
end※上記は PR の説明から再構成したイメージコードです。実際のメソッド名や引数は Selenium の実装に従います。
actionpack/test/dispatch/system_testing/driver_test.rb の変更:
- テスト側で、「プリロード時に
options.binaryが正しく設定されること」を検証するテストが追加・更新されています。 - 具体的には:
- これまで「ドライバパスが解決されているか」を見ていた箇所に加えて
- 「ブラウザバイナリパス(
options.binary)も期待どおりに設定されているか」をチェックするようになっています。
- 行数ベースでは +15/-11 程度の比較的小さな追加・修正で、既存ケースの調整+新アサーションの追加と考えられます。
- 影響範囲・注意点
- 影響対象:
- Rails の system test を Selenium(Chrome / Firefox)で動かしていて
config.system_testingで「ドライバのプリロード」を有効にしているプロジェクト- かつ Selenium 4.45 以降(
DriverFinder仕様変更後)を使っている環境
- この PR により:
- 「cannot find Chrome binary」などのエラーで system test が失敗していたケースが解消される見込みです。
- ドライバとブラウザのパス解決が同じ
DriverFinderによって一貫して行われるため、Selenium の>= 4.20.0サポート範囲内で後方互換性が保たれています。
- 注意点:
- 独自に
options.binaryを上書きしている(例: 特定の Chrome/Firefox バイナリを明示指定している)場合は、Rails 側のプリロード処理との競合がないか確認するとよいです。- ただし、この PR は「
DriverFinderが解決したブラウザパスを使う」だけなので、ほとんどのケースでは既存の期待どおりに動作します。
- ただし、この PR は「
- CI などで「Selenium Manager 無効化」「独自のドライバ/ブラウザ配置」をしている場合にも、
DriverFinderベースの解決に統一されるため、パス解決ロジックの挙動を一度確認しておくと安心です。
- 独自に
- 参考情報 (あれば)
- 元の Selenium 側 Issue:
https://github.com/SeleniumHQ/selenium/issues/17698
→DriverFinderがoptionsをミューテートしなくなった変更と、それによりoptions.binaryが設定されなくなる問題の報告。 - この PR は既存の Selenium サポート範囲(
selenium-webdriver >= 4.20.0)と互換性を保つように実装されています。
#57918 Make default scopes ractor safe
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveRecord::Base.default_scopeの実装を見直し、デフォルトスコープを Ractor セーフ(スレッド/Ractor間で共有しても安全)にするために、default_scope の内部状態を freeze して不変オブジェクトとして扱うようにした PR です。加えて、この挙動を保証するテストが追加されています。
- 変更内容の詳細
何をしたか
ActiveRecord::Base.default_scopeが内部的に保持している- 「default scope の値オブジェクト」
- 「
default_scopes配列」 を、基本的に「凍結(freeze)」して不変オブジェクトとして扱うように変更。
default scope のブロック(proc)は
instance_execで評価されるため、ユーザーにとっても Ractor 共有可能な(Ractor.shareable?な)オブジェクトにしやすいと判断し、そこを前提に安全に共有できるようにしている。default_scopesに新しい要素を追加する際は、- 既存配列を
dup - 新しい scope を
append(push) - その結果の配列を
freeze
という手順で、「配列自体もイミュータブル」となるようにしている。
- 既存配列を
疑似コードイメージ(実際のコード構造を簡略化したイメージです):
# 変更前(イメージ)
def default_scope(scope = nil, &block)
self.default_scopes ||= []
new_scope = DefaultScope.new(scope || block)
self.default_scopes << new_scope # 既存配列を破壊的に変更
end
# 変更後(イメージ)
def default_scope(scope = nil, &block)
new_scope = DefaultScope.new(scope || block).freeze # 値オブジェクトをfreeze
# default_scopes 配列は、常に freeze された新しい配列オブジェクトに差し替える
scopes = (self.default_scopes || []).dup
scopes << new_scope
self.default_scopes = scopes.freeze
endテストの追加
activerecord/test/cases/scoping/default_scoping_test.rb にテストが追加され、主に以下のようなことが確認されていると考えられます(PR説明と統計から推測):
default_scope呼び出し後に、内部で保持される default scope の値オブジェクトがfrozen? == trueであること。default_scopes配列自身もfrozen? == trueであること。- 複数回
default_scopeを呼び出しても、配列の freeze が維持されつつ、新しい要素が正しく追加されること(dup + append + freezeの挙動が正しいこと)。
- 影響範囲・注意点
影響範囲
- 対象は
ActiveRecord::Base.default_scopeの内部実装 およびそれに依存するクラスです。 - 特に以下のようなケースに関わります:
default_scopeを多用するモデル- Ractor を使った並列実行(Ruby 3 以降)を意識しているアプリ / ライブラリ
default_scopeの内部状態(default_scopes配列など)に直接アクセス・変更しているメタプログラミング的なコード
注意点
- 破壊的変更の可能性:
default_scopes配列が常にfreezeされるようになるため、Model.default_scopes << somethingModel.default_scopes.push(...)Model.default_scopes[0] = ...
といった 配列を直接書き換えるコードは
FrozenErrorで落ちる 可能性があります。これらは本来 Rails が想定していない内部構造への依存なので「非推奨な使い方」ではありますが、メタプログラミングで触っている場合は注意が必要です。
- default scope の「値オブジェクト」も
freezeされるため、- もしライブラリ側で独自に
default_scopeの値オブジェクトを取り出して mutate(プロパティを書き換え)しているような場合も同様にFrozenErrorのリスクがあります。
- もしライブラリ側で独自に
- 通常の使い方(
default_scope { where(active: true) }など)をしているアプリケーションにとっては、挙動としてはほぼ影響なく、Ractor セーフになる分だけ安全性が向上します。
- 参考情報 (あれば)
- Ruby の Ractor と共有可能オブジェクト:
Ractor.shareable?(obj)がtrueになる条件として、「オブジェクトが再帰的にすべて frozen である」ことが重要です。今回の変更はdefault_scope関連オブジェクトをその条件に近づけるものです。
- Rails 内の類似の freeze 戦略:
- 最近の Rails では、設定オブジェクトや Arel ノード、relation 関連オブジェクトで freeze を積極的に使うことで、Ractor セーフ / スレッドセーフな実装を進めており、その一環と見ることができます。
#57930 Only document the public ActionView::DependencyTracker API [ci skip]
マージ日: 2026/6/30 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::DependencyTracker のうち、実際に外部から利用されている「公開API」のみを公式ドキュメントに載せるように整理した PR です。register_trackerとERBTrackerを正式にドキュメント対象とし、それ以外の内部実装は引き続き非公開(:nodoc:)とすることで、サポート対象のエントリポイントを明確にしています。
- 変更内容の詳細
2-1. 何が「公開API」として文書化されたか
この PR では以下が「公式にサポートされる public API」として RDoc/YARD などの生成ドキュメントに現れるようになります。
ActionView::DependencyTrackerActionView::DependencyTracker.register_trackerActionView::DependencyTracker::ERBTracker
一方で、次は依然として内部用実装と位置付けられ、:nodoc: のままです。
find_dependenciesremove_tracker- 各種 tracker 用の正規表現(依存関係解析ロジックの詳細)
RubyTrackerクラス
(実際のコード変更は主にこれらに対するコメント・:nodoc: の付け外しであり、仕様や挙動を変えるロジック変更ではありません。)
2-2. register_tracker の役割
register_tracker は「新しいテンプレート言語の依存関係を ActionView に教える」ための公式な拡張ポイントです。
概ね以下のような API だと考えられます(※PR 文脈と既存の coffee-rails / haml-rails からの推測であり、実際のシグネチャは Rails 本体を参照してください):
# 例: foo という独自テンプレート拡張子向けに tracker を登録する
ActionView::DependencyTracker.register_tracker(
:foo, # テンプレートタイプ / 拡張子
MyApp::ViewDependency::FooTracker # 依存関係解析ロジック
)こうすることで、render の際に *.foo テンプレートから参照される partial / template などの依存関係を Rails が把握し、キャッシュの失効や再読み込みに反映できるようになります。
PR 説明でも触れられているように、すでに以下の gem がこの API を利用しています:
- coffee-rails
https://github.com/rails/coffee-rails/blob/v5.0.0/lib/coffee/rails/template_handler.rb#L25 - haml-rails
https://github.com/haml/haml-rails/blob/v3.0.0/lib/haml-rails.rb#L34
これらはすでに実運用されている事実上の public API でしたが、クラスごと :nodoc: になっていたため「非公開扱い」になっていました。今回それを公式に「ここが拡張ポイントです」と宣言する形です。
2-3. ERBTracker の役割
ActionView::DependencyTracker::ERBTracker は
- 「Ruby と同じような
render呼び出しを行うテンプレート言語」- 例:
render "partial",render partial: "users/user", locals: { user: @user }等
- 例:
- そのテンプレートから、どの partial / template が使われているかを解析して依存関係を抽出
するための再利用可能な tracker クラスです。
PR 説明によると、ERBTracker は「gem が自前のテンプレート言語を実装していても、その render 呼び出しが Ruby に似ているなら、そのまま使える共通の tracker」として想定されています。
例: 独自テンプレート *.foo が実質的に ERB と同じ記法で render を呼んでいるなら、
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)のように再利用すれば、独自に依存関係解析ロジックを書かなくてもよくなります。
今回の PR によって ERBTracker 自体の挙動は変わっていませんが、「ここを使ってよい・ここは拡張してよい」という意味での公式ドキュメントが追加され、サポート範囲が明文化されています。
2-4. 内部APIの非公開化の継続
find_dependencies・remove_tracker・RubyTracker・内部の正規表現などは引き続き :nodoc: とされ、API ドキュメント生成の対象外になっています。これは
- これらを「いつでも変更可能な内部実装」と位置付ける
- gem 作者に対して「ここを直接触ると将来壊れる可能性が高い」とメッセージする
という意味があります。
- 影響範囲・注意点
実行時の挙動への影響は基本的にありません
変更はほぼドキュメントレベルであり、依存関係追跡のロジックやインターフェースのシグネチャ変更は含まれていません。すでに
register_tracker/ERBTrackerを使っている gem / アプリは、そのまま利用継続可能です
これまで「実質 public だがドキュメント上は private」という微妙な状態だったものが、正式に public API として認められた形です。将来的な後方互換性も、他の public API と同様のポリシーで扱われる期待が持てます。逆に、
find_dependenciesやRubyTracker等の内部 API に依存している場合は要注意
これらは今回も明確に非公開扱いとされました。- もし独自 gem 等で直接呼んでいる場合は、
register_trackerと独自 Tracker クラス、もしくはERBTrackerの再利用に移行すべきです。 - 将来的な Rails のマイナーアップデートで破壊的変更を受ける可能性があります。
- もし独自 gem 等で直接呼んでいる場合は、
ドキュメント生成時の見え方が変わります
Rails の API ドキュメントサイト(api.rubyonrails.org等)で、ActionView::DependencyTrackerとregister_tracker/ERBTrackerが見えるようになり、拡張方法がわかりやすくなります。
- 参考情報 (あれば)
register_trackerを利用している既存 gem の例:実際に独自テンプレートエンジンを実装する場合のざっくりした構成イメージ:
# 独自テンプレートエンジン Foo の Tracker
class FooTracker
# ActionView が呼び出す想定のインターフェース
def self.call(name, template)
# template.source を解析して render 呼び出しを拾い、
# 依存しているテンプレートパスの配列を返す等
# ここは Rails 本体の実装/Doc を参照して合わせる
end
end
# 初期化時に tracker 登録
ActionView::DependencyTracker.register_tracker(:foo, FooTracker)
# もしくは render 構文が Ruby/ERB とほぼ同じなら:
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)独自エンジンを作る側は、今後はこのあたりが「公式なやり方」としてドキュメントで参照できるようになります。
#57932 Minor Active Support Cache refactorings
マージ日: 2026/6/30 | 作成者: @byroot
- 概要 (1-2文で)
ActiveSupport::Cache 周りの内部実装を少し整理したリファクタリング PR です。instrument/ログ出力処理と、ローカルキャッシュ (LocalCache) のincrement挙動が単純化され、不要なヘルパーやメソッドが削除されています。
- 変更内容の詳細
2-1. ActiveSupport::Cache#instrument の整理
やったこと
- 内部ヘルパー
_instrumentを削除し、instrumentの処理を直接書く形に変更。 - ログ出力において
logger.levelを手動で確認するのではなく、Logger#debugのブロック形式を利用するように変更。
目的・効果
_instrumentはinstrumentからしか呼ばれておらず、処理もほぼ同じだったため、メソッド分割による見通し改善よりも「追いにくさ」の方が勝っていたと判断して削除されています。logger.debug { ... }形式にすることで、デバッグログが無効なときにはブロックが評価されず、メッセージ構築コストを自動的に避けられます。
これにより、手動でif logger.debug?のようなレベルチェックを書く必要がなくなります。
イメージコード(概念的な Before / After)
Before(イメージ):
def instrument(operation, key, options = nil)
payload = { key: key }
_instrument(operation, payload, options) do
# 実処理
end
end
def _instrument(operation, payload, options)
if instrument_notifications?
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
result = yield
logger.debug("Cache #{operation}: #{payload[:key]}") if logger && logger.debug?
result
end
else
yield
end
endAfter(イメージ):
def instrument(operation, key, options = nil)
payload = { key: key }
if instrument_notifications?
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
result = yield
logger&.debug { "Cache #{operation}: #{payload[:key]}" }
result
end
else
result = yield
logger&.debug { "Cache #{operation}: #{payload[:key]}" }
result
end
end※ 上記は概念を伝えるための擬似コードであり、実際のコードとは細部が異なります。
2-2. Local Cache (ActiveSupport::Cache::Strategy::LocalCache) の increment の簡略化
変更点
- ローカルキャッシュ戦略における
increment実装がシンプルになった。 bypass_local_cacheメソッドが削除された。
以前の挙動(概念)
LocalCache 戦略は、プロセス内で使う「スレッドローカルのキャッシュ」のようなもので、元のストア(例: Redis, Memcached, MemoryStore)をラップしています。
以前は、increment を行う際に、
- 「ローカルキャッシュをバイパスして元のストアに直接
incrementを送る」
ような処理をしていたため、bypass_local_cacheといったヘルパーが存在していました。
今回の簡略化後(概念)
incrementを行うときに、わざわざローカルキャッシュをバイパスせず、よりストレートに本来のストア実装にフォワードする形に整理。- その結果、
bypass_local_cache自体が不要になり削除されています。
テスト側 (local_cache_behavior.rb) の変更から読み取れること:
increment実行時のローカルキャッシュとの整合性や、複数回呼び出し時のカウンタ挙動などについてテストが拡充されているため、挙動は維持・明確化された上で内部構造だけがシンプルになった、という位置づけです。
2-3. その他の小さな変更
memory_store.rbの微修正 (+1/-1):
ログ周り、あるいはincrement関連で統一的な呼び出しに合わせた小さなコード調整と思われます。- Redis ベースのキャッシュストアテスト
deprecated_redis_cache_store_test.rbredis_cache_store_test.rb
の両方で 1 行ずつ変更されています。主にincrementの扱い、もしくはローカルキャッシュを経由した場合の振る舞い確認のための微修正です。
- 影響範囲・注意点
外部 API の互換性
- 公開 API (
ActiveSupport::Cache::Store#increment,#fetch, etc.) の振る舞いは変えずに、内部実装を整理したリファクタリングです。 - 直接
_instrumentやbypass_local_cacheに依存していなければ、通常のアプリケーションコードに影響はありません。
- 公開 API (
モンキーパッチ / 内部依存している場合
ActiveSupport::Cacheに対して独自拡張・モンキーパッチをしている場合、以下を確認してください:_instrumentにフックしていたコードがあれば破綻します。ActiveSupport::Cache::Strategy::LocalCache::LocalStoreなどの内部クラス/bypass_local_cacheに依存していた場合も同様です。
- 特にキャッシュミドルウェアや独自キャッシュストアを書く際に、内部メソッドに依存していると影響を受ける可能性があります。
ロギング周り
- これまで
logger.debug?判定を前提にしていたようなモンキーパッチと組み合わせている場合、logger.debug { ... }形式への移行によって挙動が微妙に変わる(ログメッセージ生成タイミングなど)可能性はありますが、通常は問題にならない範囲です。
- これまで
テスト
- Local Cache 挙動を検証するテストが増えているため、
incrementの意味的な挙動は今後もこの PR の形が前提になります。 redis_cache_store関連テストも微修正されており、Redis + LocalCache 併用環境でも、想定どおりにincrementが動くことが確認されています。
- Local Cache 挙動を検証するテストが増えているため、
- 参考情報 (あれば)
Rails ガイド(キャッシュ)
https://guides.rubyonrails.org/caching_with_rails.html
ActiveSupport::Cache 全般の利用方法。今回の変更は主に内部実装であり、このガイドに書かれた使い方はそのまま有効です。ActiveSupport::Notifications とログの連携
多くのキャッシュ操作はcache_read.active_supportなどの通知イベントとして出るため、APM や独自メトリクス収集をしている場合はinstrument実装の読みやすさ向上がメンテナンス性に寄与します。
#57931 Implement ActiveSupport::ProxyLogger
マージ日: 2026/6/30 | 作成者: @byroot
- 概要 (1-2文で)
RailsのActiveSupportにActiveSupport::ProxyLoggerが追加され、既存のロガーにログを委譲しつつ「別のログレベルで」フィルタリングできるラッパーロガーが使えるようになりました。これにより、アプリ全体と同じ出力先を使いつつ、特定ライブラリだけログを抑制・制御することが容易になります。
- 変更内容の詳細
2-1. ProxyLoggerとは何か
ActiveSupport::ProxyLogger は「あるロガー(親ロガー)」へログ出力を“丸ごと委譲”しつつ、自身が持つ独立したログレベルでログを間引くクラスです。
SomeLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)この例では:
- 実際の出力先・フォーマットなどは
Rails.loggerをそのまま利用 - しかし
SomeLibraryから出るログは「error 以上」に制限される
→infoやdebugは、ProxyLogger 側で捨てられる
アプリ本体のログレベル(例: :info)と、ライブラリのログレベル(例: :error)を分けられるのが主な目的です。
2-2. 対応しているインターフェース
PR 説明によると、標準ライブラリ Logger のインターフェースのほとんどをサポートしています。典型的には以下のようなメソッドが委譲・対応されていると考えてよいです(実装ファイルは active_support/proxy_logger.rb):
- レベル指定メソッド:
debug,info,warn,error,fatal,unknown
- 汎用ログメソッド:
add(severity, message = nil, progname = nil, &block)
- ログレベル関連:
level,level=,debug?,info?,warn?,error?,fatal?
- その他
Logger互換・ActiveSupport::Logger 互換のメソッドがほぼそのまま使える想定
挙動のイメージ:
# Rails.logger は通常通りアプリ全体のロガー
Rails.logger.level = :info
# noisy ライブラリはログが多いので、ライブラリ側だけ error 以上に絞る
NoisyLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
NoisyLibrary.logger.info("This will NOT be logged") # ProxyLogger のレベル :error により破棄
NoisyLibrary.logger.error("Something bad") # Rails.logger へ error として出力親ロガー (Rails.logger) のログレベルや出力先には影響を与えず、ProxyLogger 側だけでフィルタリングしているのがポイントです。
2-3. 実装位置・その他の変更
- 新クラス:
activesupport/lib/active_support/proxy_logger.rbに実装(176行追加)
- 読み込み:
active_support.rbにrequireが追加され、ActiveSupport をロードすれば利用可能
- 既存 Logger 周りの微調整:
active_support/logger.rbが 1 行だけ削除されており、内部の読み込み順や依存関係の整理と思われます
- テスト:
activesupport/test/proxy_logger_test.rbが追加され、主な挙動(ログレベル判定・委譲)がテストされています
- ドキュメント:
activesupport/CHANGELOG.mdにエントリ追加
- 影響範囲・注意点
3-1. 影響範囲
- 新機能であり、既存コードは基本的に非互換変更の影響を受けません。
- 既存の
Rails.logger/ActiveSupport::Loggerの挙動も変わりません。 - 外部ライブラリや自前コードで「ロガーを差し替えるポイント」があれば、そこで
ProxyLoggerを差し込むことができます。
3-2. 利用時の注意点・設計上の考慮
ログレベルの二重管理
- 親ロガーと ProxyLogger の両方にログレベルが存在します。
- 実際に出力されるかどうかは「ProxyLogger のレベル判定を通過し」「かつ親ロガー側で弾かれない」場合のみです。
- 例: 親が
:error、ProxyLogger が:infoだと、infoログは ProxyLogger 側で通っても親で捨てられます。
出力先・フォーマット変更は親ロガー側で行う
ProxyLoggerは基本的に委譲しかしないため、- ログの出力先(ファイル・標準出力・外部サービスなど)
- フォーマッタ、タグ付け、ログサブスクライバ
などはすべて「親ロガー側」で設定・制御します。
ライブラリのロガー差し替えが可能か確認
SomeLibrary.logger = ...のようにロガーを外から渡せる前提の仕組みを持つライブラリでのみ、この機能が直接活きます。- Rails の各種コンポーネント(ActiveRecord, ActionController など)や、Sidekiq など多くの gem がこのパターンを取っています。
パフォーマンス観点
- ProxyLogger で枝刈りすることにより、特に noisy なライブラリが
debugやinfoを大量に発行する場合に、出力やフォーマット処理の負荷を下げられます。 - ただしログメッセージ構築自体(文字列連結など)が呼び出し元で行われている場合、そのコストは残るため、可能ならブロック形式のログ (
logger.debug { expensive_computation }) を使うのが望ましいです。
- ProxyLogger で枝刈りすることにより、特に noisy なライブラリが
- 参考情報 (あれば)
- 追加されたクラス:
activesupport/lib/active_support/proxy_logger.rb
→ 実装詳細・対応しているメソッド一覧を確認可能 - 変更履歴:
activesupport/CHANGELOG.mdの ActiveSupport セクション - 利用イメージまとめ:ruby
# アプリ本体 Rails.logger.level = :info # ライブラリAはログがうるさいので error 以上だけ LibraryA.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error) # ライブラリBは debug も見たい LibraryB.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :debug)
このPRにより、「同じ出力先を共有しつつ、コンポーネントごとにログレベルを変える」というパターンが、Rails 標準の API だけでシンプルに書けるようになりました。
#57874 Make the Action View template handler registry Ractor-shareable
マージ日: 2026/6/30 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Action View のテンプレートハンドラのレジストリを Ractor で共有可能(Ractor-shareable)にするため、内部実装がクラス変数ベースからモジュールインスタンス変数+不変(frozen)な構造に再設計された PR です。これにより、テンプレート検索・描画パスでのハンドラ参照が Ractor セーフかつスレッド安全になり、読み取り時に書き込みが発生しないようになっています。
- 変更内容の詳細
2-1. レジストリの格納場所の変更
従来:
ActionView::Template::HandlersをTemplateにextendし、- ハンドラのレジストリ(extension → handler)は クラス変数 に保持されていました。
- そのため、Ractor 的には共有しづらく、「読み取り時にレジストリを書き換える」ようなパターンも残っていました(遅延メモ化など)。
変更後:
- レジストリは
Template::Handlersモジュールの モジュールインスタンス変数 に保持されます。 Handlersモジュール自体が Ractor 間で共有され、非メイン Ractor からも「共有可能な値」として安全に読めるように設計されています。Template側の API (Template.handler_for_extension,Template::Handlers.extensionsなど) は、モジュール上の実装に委譲する薄いラッパーになりました。Templateは依然としてHandlersを extend していますが、実体はモジュール側に寄せた形です。
2-2. ハンドラオブジェクトを Ractor-shareable に
Ractor 間で共有するには、保持されるオブジェクト(ハンドラ)自体も shareable である必要があります。この PR では以下のように処理しています。
組み込みハンドラ
- 例: ERB ハンドラ、Builder ハンドラなど
- これらのインスタンスを
freezeして不変オブジェクトに。 - 不変にすることで、Ractor 間で安全に共有可能となります。
:rubyハンドラ:rubyテンプレート用のハンドラはActiveSupport::Ractors.shareable_lambdaを使って生成。shareable_lambdaは、Ractor 間で共有できるProc/lambdaを作るための ActiveSupport ヘルパです。
アプリケーション側で登録される Proc ハンドラ
- アプリケーションコードから
ActionView::Template.register_template_handlerなどで登録されるProc/lambdaについては、ActiveSupport::Ractors.try_shareable_procを経由して登録します。
try_shareable_procは、渡された Proc を shareable にできる場合はそうし、そうでなければ(たとえばクロージャで非共有な外部オブジェクトを閉じている場合など)適切に扱うためのヘルパです。- これにより、「アプリケーションが登録したハンドラが Ractor 的に問題を起こす」ケースを減らしつつ、できるだけ共有可能にする方針になっています。
- アプリケーションコードから
2-3. レジストリ更新方式の変更(コピーオンライト & イager 計算)
以前の問題点:
Template::Handlers.extensionsなどが「遅延メモ化(memoization)」しており、読み取りのタイミングで内部状態を書き換える可能性がありました。- Ractor・並行性の観点からは、読み取りが決して書き込みを行わない構造が望ましいです。
変更後:
レジストリ(extension → handler の Hash)は 登録時に新しい frozen Hash を作るコピーオンライト方式に変更されました。
例イメージ(実装イメージ・擬似コード):
rubydef register_handler(ext, handler) @handlers = @handlers.merge(ext.to_s => handler).freeze @extensions = @handlers.keys.freeze end
Template::Handlers.extensionsの内容(拡張子の一覧)も、- 登録時に毎回再計算して freeze
- 読み取りは計算済みの frozen オブジェクトを返すだけ
これにより、「読み取り時には一切書き込みが発生しない」設計となり、Ractor/並行実行での安全性と予測可能性が向上しています。
2-4. 初期登録の場所変更 (self.extended フックを排除)
従来:
TemplateがHandlersを extend する際のself.extendedフック(モジュールの extend 時に呼ばれるフック)内で、組み込みハンドラの初期登録を行っていました。
変更後:
- ハンドラのレジストリと登録ロジックはすべて
Handlersモジュール自身に集約されました。 - 組み込みハンドラは モジュール読み込み時に直接登録されます。
self.extendedフックは不要となり削除されています。- テスト用・公開 API としての
ActionView::Template.*インスタンスメソッドは、単にHandlersに委譲するだけです。
- テスト用・公開 API としての
2-5. Builder ハンドラの require メモ化方式変更
ActionView::Template::Handlers::Builder では、
- 以前は「
require "builder"をしたかどうか」を インスタンス変数でメモ化していました。 - しかし、この PR により Builder ハンドラも
freezeされるため、インスタンス変数への書き込み(=メモ化)が不可能になります。
そこで、
「
require "builder"が既に実行されたかどうか」をdefined?ガードで判定する方式に変えています。例イメージ(概念的なもの):
rubydef call(...) require "builder" unless defined?(::Builder) ... end
これにより、ハンドラインスタンス自体は完全に不変のまま、
requireの多重実行を避けることができます。
2-6. テストとツールの更新
actionview/test/template/handlers_test.rbに、Ractor-shareable な動作や新たな登録ロジックを検証するテストが追加。actionview/test/template/render_test.rbは、挙動変更に伴う小さな修正のみ。tools/strict_warnings.rbでも、今回の実装変更で発生する警告に対応するための微修正が入っています。
- 影響範囲・注意点
3-1. Ractor/並行実行まわり
- テンプレートハンドラのレジストリとハンドラオブジェクトが Ractor-shareable になったことで、
- 将来の Action View の Ractor 対応、
- マルチ Ractor でのレンダリング・テンプレート解決
への布石となります。
- 「読み取り時に書き込みが発生しない」ようになったため、従来まれに起こり得た競合や予期しない挙動がさらに減ります。
3-2. カスタムテンプレートハンドラを定義しているアプリケーションへの影響
アプリケーション側で Proc/lambda ベースのカスタムテンプレートハンドラを登録している場合:
- 登録処理は
ActiveSupport::Ractors.try_shareable_procを通るようになっています。 - 一般的なハンドラ(トップレベル定義のメソッドや、外部の Ractor 非共有な状態を閉じていないラムダ)であれば、そのまま問題なく動作するはずです。
- ただし、以下のようなケースでは Ractor-shareable にならない可能性があります:
- クラスインスタンス変数やスレッドローカルなど、「Ractor 非共有なオブジェクト」を閉じた Proc
- ミューテーブルな状態に依存するハンドラ
- その場合でも動作自体は維持される想定ですが、将来の完全な Ractor 対応の観点からは、
- ハンドラをできるだけ不変/再入可能に設計する、
- Ractor 非共有な状態を閉じないようにする
ことが推奨されます。
3-3. Builder ハンドラの挙動
Builderハンドラはrequire "builder"のメモ化の仕方が変わりましたが、- Ruby の
require自体が多重呼び出しに耐性があること、 - 実際には
defined?によるガードで一度だけ読まれることから、
- Ruby の
- アプリケーション側から見た挙動に変化は基本的にありません。
- 参考情報 (あれば)
- この PR で導入・利用されている Ractor 関連 API:
ActiveSupport::Ractors.shareable_lambdaActiveSupport::Ractors.try_shareable_proc
- 関連しそうなドキュメント/コード:
actionview/lib/action_view/template/handlers.rbactionview/lib/action_view/template/handlers/builder.rbactionview/test/template/handlers_test.rb
→ 新しいレジストリ実装の実例として読むと理解しやすいです。
#57916 Treat a Symbol stream name as a literal stream in channel test assertions
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のチャンネル用テストヘルパ (ActionCable::Channel::TestCase) において、シンボルで指定したストリーム名を本番動作と同じく「文字列に対応するリテラルなストリーム名」として扱うように修正した PR です。これにより、:my_roomと"my_room"を使ったストリーム/アサーションの挙動が一貫し、誤検知・見逃しが起きないようになります。
- 変更内容の詳細
背景: 本番環境での挙動
本番では以下のようなコードは:
stream_from :my_room内部的には "my_room" というリテラルなストリーム名に対して購読します。
つまり:
stream_from :my_room
stream_from "my_room"は同じストリームを指します。
また ActionCable.server.broadcast も、シンボル・文字列を同等に扱うストリーム名としてサポートしています。
問題となっていたテストアサーションの挙動
ActionCable::Channel::TestCase の以下のアサーションだけが、この仕様とズレていました:
assert_broadcasts(:my_room)assert_broadcast_on(:my_room)assert_has_stream(:my_room)assert_has_no_stream(:my_room)
1. assert_broadcasts / assert_broadcast_on の問題
シンボルで書いたときだけ、チャンネルスコープ付きの名前 に変換されていました。
assert_broadcasts(:my_room, 1) do
ActionCable.server.broadcast("my_room", { text: "hi" })
end
# エラーメッセージ:
# 1 broadcasts to my_channel:my_room expected, but 0 were sent.- 実際に送っているのは
"my_room"へのブロードキャスト - しかしアサーション側は
my_channel:my_roomという別のストリームを見てしまう
→ その結果、「0件しか送られていない」と誤判定。
文字列の場合は元々正しく動いており:
assert_broadcasts("my_room", 1) do
ActionCable.server.broadcast("my_room", { text: "hi" })
end
# これは成功していた2. assert_has_stream / assert_has_no_stream の問題
類似の問題がストリーム存在チェック系にもありました。
# 実際には stream_from :my_room が有効
assert_has_stream(:my_room)
# => "Stream my_room has not been started" で失敗逆に:
# 実際には stream_from :my_room が有効
assert_has_no_stream(:my_room)
# => 何も検出できず成功してしまう(false green)これは #57698 で指摘された assert_no_broadcasts と同じく、「テストが緑なのに本当は何も検証できていない」という危険な失敗パターンです。
具体的な修正内容
actioncable/lib/action_cable/channel/test_case.rb にて、以下のようなポリシーに統一:
シンボルで指定されたストリーム名は、テストアサーション内でも 等価な文字列として扱う。
つまり:
:my_room→"my_room"に変換し、チャンネルスコープを付けず にそのまま使う- これにより本番の
stream_from/broadcastと同じストリーム名解釈となる
結果として、次のように書いたとき:
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from :my_room
end
endテストで:
assert_has_stream :my_room # OK
assert_has_stream "my_room" # OK
assert_broadcasts :my_room, 1 do
ActionCable.server.broadcast(:my_room, text: "hi")
end
assert_broadcasts "my_room", 1 do
ActionCable.server.broadcast("my_room", text: "hi")
endといった書き方がいずれも期待どおり動きます。
テストの追加
actioncable/test/channel/test_case_test.rb に45行分のテストが追加され、主に以下をカバーしています:
- シンボル指定の
assert_broadcasts/assert_broadcast_onが、文字列指定と同じストリームを検証すること - シンボル指定の
assert_has_stream/assert_has_no_streamも、実際のストリーム開始状況と一致して判定されること - 既存の文字列パスが壊れていないこと
- 影響範囲・注意点
対象: Action Cable のチャンネルテスト (
ActionCable::Channel::TestCase) を書いているコードベース本番挙動への影響: なし(本番とテストのズレを埋める変更)
破壊的変更の可能性:
- これまで「たまたま」緑になっていたテストが、本来の挙動に合わせてレッドになる 可能性があります。
- 特に、以下のようなコードを書いていたプロジェクトは要注意です:ruby以前は「たまたま」
# 実際には my_channel:my_room にしかブロードキャストしていない stream_from "my_channel:my_room" # しかしテストで :my_room を想定していた assert_broadcasts :my_room, 1 do ActionCable.server.broadcast("my_channel:my_room", payload) end:my_roomがmy_channel:my_roomにスコープされて通っていたものが、
今後は"my_room"を指すようになるため失敗します。 - その場合は、明示的に望むストリーム名を文字列で指定する か、
テストと実装のストリーム名を揃える(:my_room/"my_room"いずれかに統一)必要があります。
ベストプラクティス:
- 本番コードでシンボルを使っているなら、テストも同じシンボル/文字列に揃える
- 「チャンネル名を含んだフルネーム」をあえて使いたいなら、必ず文字列で明示する:ruby
stream_from "chat_channel:my_room" assert_broadcasts "chat_channel:my_room", 1 do # ... end
- 参考情報 (あれば)
- PR本体: #57916 “Treat a Symbol stream name as a literal stream in channel test assertions”
- 関連PR:
- #57698: テストアダプタの
assert_no_broadcastsにおけるシンボル/文字列非対称問題の修正 - #57703, #57704: 同じファイル内の周辺レイヤーでの、シンボル vs 文字列非対称を個別に修正した PR(本PRで統合)
- #57698: テストアダプタの
#57912 Reference ActionCable::Configuration in the Action Cable guide
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
Action Cable ガイド内の「Configuration」セクションで参照していたクラス名を、古いActionCable::Server::Configurationから現在の正規名ActionCable::Configurationに修正したドキュメント更新です。バックワードコンパチ目的のエイリアスは残っていますが、ガイド上は新しい正規クラス名を指すようになりました。
- 変更内容の詳細
対象ファイル:
guides/source/action_cable_overview.md
変更内容は 1 行のみで、Action Cable の設定項目一覧へ誘導する箇所のクラス名を修正しています。
従来 (概念的なイメージ):
See `ActionCable::Server::Configuration` for the full list of configuration options.修正後:
See `ActionCable::Configuration` for the full list of configuration options.つまり、ドキュメント中で「全設定オプションの一覧は ActionCable::Server::Configuration を見てください」と案内していたのを、「ActionCable::Configuration を見てください」という案内に更新しただけの変更です。
Rails 本体側では、ActionCable::Configuration がトップレベルの正規クラスであり、ActionCable::Server::Configuration は互換性維持用のエイリアスとして残っている状態になっている前提です。この PR は、その実装にドキュメントを追従させたものです。
- 影響範囲・注意点
- 影響対象:
- 公式ガイドを見て設定方法を確認する開発者
ガイドに書かれているクラス名が、実際のコードベースが採用している正規の名前 (ActionCable::Configuration) と一致するようになります。
- 公式ガイドを見て設定方法を確認する開発者
- 実コードへの影響:
- コードの挙動や API 仕様そのものは変更されていません。
ActionCable::Server::Configurationは引き続きエイリアスとして存在するため、これを参照している既存コードが壊れることはありません。
- 注意点:
- 新規実装やリファクタリング時には、今後は
ActionCable::Configurationを前提にコードやドキュメントを書くのが推奨されます。 - 既存の記事・ブログ・社内ドキュメントなどで
ActionCable::Server::Configurationを紹介している場合、Rails の現行バージョンに合わせて表記だけActionCable::Configurationに更新するのが望ましいです。
- 新規実装やリファクタリング時には、今後は
- 参考情報 (あれば)
- Action Cable ガイド (Configuration セクション):
guides/source/action_cable_overview.mdの設定セクション - 関連する実装 (推定箇所):
ActionCable::Configurationクラス定義ActionCable::Server::Configuration = ActionCable::Configurationのようなエイリアス定義 (互換性維持用)
#57928 Eliminate a useless frame in AS::Notifications.instrument
マージ日: 2026/6/30 | 作成者: @byroot
- 概要 (1-2文で)
ActiveSupport::Notifications.instrument内の「無駄なスタックフレーム」を取り除き、スタックトレースやフレームグラフが少し読みやすくなるようにした変更です。性能への影響はほぼありませんが、デバッグやプロファイリング時の視認性が向上します。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/notifications.rb(+2/-2)
ActiveSupport::Notifications.instrument の実装から、実行上ほとんど意味を持たない「余計なメソッド呼び出し」や「余計なブロックラップ」を削り、スタックフレームを 1 段減らしています。
典型的には、以下のような構造になっていたものを:
def instrument(name, payload = {})
if notifier.listening?(name)
_instrument(name, payload) do
yield
end
else
yield
end
end
def _instrument(name, payload)
# 実際の計測・通知処理
endこれを、余計な中継レイヤをなくすような形に寄せています:
def instrument(name, payload = {})
if notifier.listening?(name)
# ここで直接計測処理を行う or 余計なラップを減らす
notifier.start(name, payload)
begin
yield
ensure
notifier.finish(name, payload)
end
else
yield
end
end実際の差分は +2/-2 行と非常に小さいもので、主な意図は「メソッドのネストを 1 段浅くする」「ブロック経由の無駄なコールを消す」ことで、結果的に:
- 例外発生時のバックトレースに出てくるフレームが 1 つ減る
- stackprof / rbspy / flamegraph などのプロファイリングツールでのコールスタック表示が少し簡潔になる
という効果があります。
- 影響範囲・注意点
- 公開 API (
ActiveSupport::Notifications.instrument) の挙動やシグネチャには変更なし - 通知されるイベント名・payload・タイミングも同一で、機能的な互換性は保たれています
- 変更はごく小さく、かつ「中継用のフレーム削除」にとどまるため、通常のアプリケーションコードが影響を受ける可能性はほぼありません
- backtrace を前提にした特殊な解析ツールや、スタックフレーム数に強く依存しているようなテストを組んでいる場合のみ、フレーム 1 つ分の違いが出る可能性があります
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57928
ActiveSupport::Notificationsのドキュメント(使い方の前提理解に):
https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html
#57919 Make LogSubscriber.log_levels ractor safe
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::LogSubscriberのsubscribe_log_levelが Ractor(マルチスレッド/マルチ Ractor)環境でも安全に動作するように、内部で持っているlog_levelsハッシュをフリーズするようにした PR です。- これにより、Ractor 間で
LogSubscriberを共有してもログレベル設定が原因でエラーやデータ競合が起きにくくなります。
- 変更内容の詳細
変更の主旨
元々、LogSubscriber には以下2点がありました:
- ログレベル判定用の定数
LEVEL_CHECKSはすでにfreeze済み - しかし、各サブスクライバごとに保持している
log_levelsハッシュはミューテーブルであり、Ractor セーフではない
この PR では、subscribe_log_level 呼び出し時に、この log_levels をフリーズするようにしています。すなわち、
- ハッシュを更新してから
freezeする - あるいは更新結果として新しい frozen ハッシュを持つようにする
という形を取ることで、Ractor が要求する「オブジェクトが不変であること」の条件を満たします。
具体的なイメージ
元のコード (イメージ):
def subscribe_log_level(*log_levels)
# self.log_levels は Hash
self.log_levels.merge!(derived_log_levels_from_args(log_levels))
endRactor では、Ractor 間で共有されるオブジェクトは基本的に frozen? である必要があるため、このままだと:
- ある Ractor で
log_levelsを更新 - 別の Ractor と共有しているときに Ractor セーフではなくなる
といった問題が起こりえます。
この PR では、以下のような形の変更がなされていると考えられます(正確なコードは PR 本文では省略されていますが、内容からの推測です):
def subscribe_log_level(*log_levels)
self.log_levels = self.log_levels.merge(derived_log_levels_from_args(log_levels)).freeze
endポイント:
merge!のような破壊的更新ではなく、新しい Hash をmergeで生成- 生成した Hash を
freeze log_levelsには常に frozen Hash を持たせるようにする
テスト (activesupport/test/log_subscriber_test.rb) も追加されており:
subscribe_log_level実行後にlog_levelsがfrozen?であること- 複数回呼び出しても正しいマージ結果と frozen 状態が保たれていること
といったことが検証されていると考えられます。
- 影響範囲・注意点
影響を受けるのは
ActiveSupport::LogSubscriberのsubscribe_log_levelを利用しているコードのみです。log_levelsが常に frozen Hash になるため、公開 API からlog_levelsを直接破壊的に書き換えていた場合はエラーになります。例(これまでなんとなく動いていたが、今後は壊れる可能性があるコード):
rubysubscriber = MyLogSubscriber.new subscriber.subscribe_log_level(:info) # こういった破壊的更新は frozen になった Hash に対しては例外を投げる subscriber.log_levels[:debug] = true # => FrozenError subscriber.log_levels.merge!(...) # => FrozenError望ましい書き方としては、
subscribe_log_levelの API を通して設定するか、どうしても差し替えたい場合は新しい Hash を作って setter に渡す形です:rubysubscriber.log_levels = subscriber.log_levels.merge(debug: true).freeze # ただし、このような使い方自体も将来の仕様変更に脆いので、可能なら避けるLogSubscriberを Ractor 間で共有するケース(あるいは Rails ロガー周りを Ractor 内で使うケース)では、今回の変更により Ractor セーフティが向上します。CHANGELOG の更新は行われていないため、「挙動変更」というよりは「安全性向上のための内部的変更」という扱いになっています。
- 参考情報 (あれば)
- Ruby の Ractor 仕様上、Ractor 間で共有されるオブジェクトは原則として immutable(
freeze)である必要があります。 - 以前から
LEVEL_CHECKS定数は freeze 済みでしたが、今回の PR によってlog_levelsも同様に不変化され、LogSubscriber全体として Ractor フレンドリーな設計に近づいています。 - マルチ Ractor 対応を見据えた Rails の内部改善の一環と位置付けられます。
#57920 Eagerly assign ExecutionWrapper's active key
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1-2文で)
ExecutionWrapper のサブクラスにおけるactive_keyの代入タイミングを「継承時」に早めることで、最初のリクエストが非メイン Ractor で処理された場合に発生する Ractor の隔離エラーを防ぐ修正です。Ractor 対応を強化するための内部的な初期化順序の見直しになります。
- 変更内容の詳細
背景
Ruby の Ractor では、「メイン Ractor 以外からクラスインスタンス変数を初期設定する」といった操作に制約があり、適切に扱わないと「ractor isolation error(Ractor の隔離違反)」が発生します。
ExecutionWrapper は Rails の実行コンテキスト(ミドルウェア的な周辺処理)を扱うためのクラスで、そのサブクラスごとに active_key というクラスインスタンス変数を持ちます。
従来は、この active_key が「必要になったタイミング(=リクエスト処理時など)」で初期化されていたため、最初のリクエストがメイン Ractor ではない Ractor で処理されるケースで Ractor の制約に引っかかる可能性がありました。
今回の変更
active_keyの代入を 「サブクラス定義時 (継承フックinherited内)」に移動 しました。- これにより、ExecutionWrapper を継承したクラスがロードされた瞬間、まだメイン Ractor 上にいる段階で
active_keyが確実にセットされます。
実装イメージ(簡略化した擬似コード):
class ActiveSupport::ExecutionWrapper
class << self
# もともとどこか別のタイミングで行っていた active_key 代入を
# 継承時フックに移したイメージ
def inherited(subclass)
super
subclass.instance_variable_set(:@active_key, :"active_#{subclass.name}")
end
end
end※上記はあくまで概念的なサンプルです。実際のキー名や代入方法はコードに依存しますが、ポイントは「inherited フック内でサブクラスのクラスインスタンス変数をセットする」ことです。
- 影響範囲・注意点
- 主な影響範囲
ActiveSupport::ExecutionWrapperを継承しているクラス(およびそれを利用するコード全般)- マルチ Ractor 環境で Rails を動かしているアプリケーション
- 期待される効果
- 最初のリクエストがメイン Ractor 以外で処理された場合でも、
active_keyに関する Ractor 隔離エラーを防げる。 - ExecutionWrapper の初期化がより決定的(deterministic)になり、Ractor 利用時の不定動作の可能性が下がる。
- 最初のリクエストがメイン Ractor 以外で処理された場合でも、
- 互換性・既存コードへの影響
active_keyの値や外部 API が変わるわけではなく、「いつセットされるか」だけが変わるため、通常のアプリケーションコードへの影響はほぼありません。ExecutionWrapperのサブクラス化や@active_keyに対してメタプログラミングを行っているような高度なコードがある場合は、初期化タイミングの違いに注意してください(ただしそのようなケースは稀と思われます)。
- パフォーマンス
- 継承時にクラスインスタンス変数を一つセットするだけなので、事実上無視できるレベルのオーバーヘッドです。
- 参考情報 (あれば)
- Ruby Ractor の制約として、「Ractor 間で共有されるオブジェクトや、Ractor 境界を跨いだミューテーション」に厳しい制約があり、クラスインスタンス変数の扱いにも注意が必要になります。
inheritedフックは Ruby における標準的なメタプログラミングフックで、class Child < Parent; endのようにサブクラスが定義されたときにParent.inherited(Child)が呼ばれます。
今回はこの仕組みを利用して、「サブクラスロード時に Ractor セーフな形で内部状態 (active_key) を初期化する」ようにしています。