ExecutionContext.after_change を Ractor セーフに実装
ActiveSupport::ExecutionContext.after_change が保持するコールバック配列を不変化し、Ractor で安全に共有できるように変更されました。既存の API はそのまま動作し、フレームワーク全体の Ractor 対応が一歩前進します。
背景
ExecutionContext の after_change コールバックは単なる Proc の配列で、Ractor 間で共有できないという問題がありました。 Ractor はオブジェクトの共有可能性を厳格にチェックし、共有不可能な Proc が渡されると例外が発生します。現在の実装では @after_change_callbacks が可変の配列であり、各要素はそのままの Proc です。そのため、Ractor 環境でコールバックが呼び出されると安全性が保証されません。この隠れた非安全領域を解消することが本 PR の主目的です。
利用頻度は限定的でドキュメント化もされていませんが、内部的に ActiveRecord のクエリログクリアなどで使用されています。 小さな使用箇所でも Ractor で例外が起きるリスクは無視できません。従来は開発者が手動で shareable にする必要があり、統一的な対策が欠如していました。安全な実装に統一することで、将来的な拡張や他コンポーネントへの波及効果が期待できます。
技術的な変更
@after_change_callbacks の初期化と更新ロジックが不変・凍結された配列に置き換えられました。 activesupport/lib/active_support/execution_context.rb では初期化を [].freeze に変更し、after_change メソッドは [@*after_change_callbacks, block].freeze で新しい配列を作成して再代入します。
@@
- @after_change_callbacks = []
+ @after_change_callbacks = [].freeze
@@
- @after_change_callbacks << block
+ @after_change_callbacks = [*@after_change_callbacks, block].freeze
この変更により配列自体が凍結され、Ractor が要求する「共有可能なオブジェクト」条件を満たします。既存コードは after_change の呼び出し方を変更せずにそのまま利用できます。
コールバックを登録する側も Ractor 共有可能な Proc にラップする必要があります。 activerecord/lib/active_record/query_logs.rb では従来の after_change { … } を ActiveSupport::Ractors.shareable_proc でラップした形に置き換えました。
@@
- ActiveSupport::ExecutionContext.after_change { ActiveRecord::QueryLogs.clear_cache }
+ ActiveSupport::ExecutionContext.after_change(&ActiveSupport::Ractors.shareable_proc { ActiveRecord::QueryLogs.clear_cache })
shareable_proc により生成される Proc は Ractor が安全に参照できることが保証され、実行時エラーが防止されます。このラップは明示的な意図表示でもあり、他のコンポーネントでも同様のパターンを採用しやすくなります。
テストコードが Ractor 共有可能性を検証するよう拡張されました。 activesupport/test/execution_context_test.rb に active_support/testing/ractors_assertions の require と include ActiveSupport::Testing::RactorsAssertions が追加され、新たに「callbacks are ractor safe」テストが実装されています。
+require "active_support/testing/ractors_assertions"
@@
include ActiveSupport::ExecutionContext::TestHelper
+ include ActiveSupport::Testing::RactorsAssertions
@@
+ test "callbacks are ractor safe" do
+ ActiveSupport::ExecutionContext.with(after_change_callbacks: [].freeze) do
+ ActiveSupport::ExecutionContext.after_change(&ActiveSupport::Ractors.shareable_proc { })
+
+ assert_ractor_shareable ActiveSupport::ExecutionContext.after_change_callbacks
+ end
+ end
追加されたテストは assert_ractor_shareable を用いて、after_change_callbacks が凍結された配列かつ Ractor で共有可能であることを自動的に検証します。テストがパスすれば、実装が期待通り Ractor セーフであることが保証されます。
設計判断
配列を凍結し、新しい配列で置き換えるイミュータブル操作は、既存 API 互換性を保つ最小侵害の選択です。 以前は << で直接配列を変更していたため、Ractor では不変性が欠如していました。凍結された配列に対しては << が禁じられるため、誤ったミューテーションを防止しつつ、after_change のシグネチャは変更せずに利用できます。結果として、既存コードは修正不要で安全性だけが向上します。
ActiveSupport::Ractors.shareable_proc の導入は、開発者に意図的な共有を明示させる設計です。 ラップ無しの Proc が渡された場合は Ractor で例外になるため、明示的にラップすることで意図がコード上に残ります。これにより将来的なバグを防ぎ、フレームワーク全体で共有可能なコールバック作成のベストプラクティスが確立されます。他コンポーネントが同様のパターンを踏襲しやすくなる副次的効果があります。
まとめ
本 PR は ExecutionContext.after_change の内部データ構造を不変・凍結に改め、 そして shareable_proc でラップしたコールバックを登録 することで、Ractor 環境でも例外なく動作できるようにしました。API 互換性は維持され、追加テストで安全性が検証されたため、Rails の Ractor 対応が着実に前進します。