ラムダ定数を Ractor 共有可能にするラップ追加
Action Pack の定数に格納されたラムダを ActiveSupport::Ractors.shareable_lambda でラップし、Ractor 環境でも安全に共有できるようにしました。機能的な変更はなく、既存の振る舞いをそのまま保ちつつマルチスレッド安全性を確保します。
背景
Ractor が導入された Ruby では、オブジェクトはコピー可能または共有可能である必要があります。Action Pack 内ではミドルウェアの判定やパラメータ解析、URL 生成などで多数のラムダが定数として保持されており、これらはデフォルトでは Ractor 間で共有できません。
定数に格納されたラムダは多数のコードパスで直接呼び出されるため、個別にリファクタリングするコストが高く、既存 API の互換性を保つことが重要です。そのため、振る舞いを変えずに共有可能にする手段が求められました。
この課題に対処するため、ActiveSupport::Ractors.shareable_lambda を用いたラップ方式が採用されました。
技術的な変更
対象となる定数ラムダはすべて ActiveSupport::Ractors.shareable_lambda でラップされ、Ruby 4.0 以降で共有可能な Proc が生成されます。Ruby 3 系では従来と同等の動作を維持します。
- INCLUDE = ->(list, action) { list.include? action }
- EXCLUDE = ->(list, action) { !list.include? action }
- NULL = ->(list, action) { true }
+ INCLUDE = ActiveSupport::Ractors.shareable_lambda(&->(list, action) { list.include? action })
+ EXCLUDE = ActiveSupport::Ractors.shareable_lambda(&->(list, action) { !list.include? action })
+ NULL = ActiveSupport::Ractors.shareable_lambda(&->(list, action) { true })
- Mime[:json].symbol => -> (raw_post) {
+ Mime[:json].symbol => ActiveSupport::Ractors.shareable_lambda(&-> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
data.is_a?(Hash) ? data : { _json: data }
- }
+ })
- ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
- ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
+ ESCAPE_PATH = ActiveSupport::Ractors.shareable_lambda(&->(value) { Router::Utils.escape_path(value) })
+ ESCAPE_SEGMENT = ActiveSupport::Ractors.shareable_lambda(&->(value) { Router::Utils.escape_segment(value) })
- SERVE = ->(app, req) { app.serve req }
- CALL = ->(app, req) { app.call req.env }
+ SERVE = ActiveSupport::Ractors.shareable_lambda(&->(app, req) { app.serve req })
+ CALL = ActiveSupport::Ractors.shareable_lambda(&->(app, req) { app.call req.env })
- PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
- UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
+ PATH = ActiveSupport::Ractors.shareable_lambda(&->(options) { ActionDispatch::Http::URL.path_for(options) })
+ UNKNOWN = ActiveSupport::Ractors.shareable_lambda(&->(options) { ActionDispatch::Http::URL.url_for(options) })
これらの変更はラップのみで実装され、ラムダ本体のロジックは一切変更されていません。そのため、既存のテストはすべてパスし、リクエスト処理や URL ヘルパーの振る舞いに差分は生じません。
設計判断
ラップのみで共有可能にする という方針は、既存 API の互換性を最小限の侵入で保つことが目的です。代替案としては個別にコードをリファクタリングし外部クラスへ移す方法が考えられましたが、全定数ラムダに対して一括適用できるユーティリティを使う方が保守コストを低減します。
ActiveSupport::Ractors.shareable_lambda は Ruby 4.0 以降で共有可能な Proc を生成し、旧バージョンでは従来通りの Proc を返すため、バックワード互換性が確保されます。これにより、Ractor 未使用のアプリケーションや古い Ruby バージョンでの動作に影響を与えません。
この設計は「最小限の変更で新しい並行実行モデルをサポートする」Rails の設計哲学と一致しています。
まとめ
本 PR は Action Pack 内の定数ラムダを ActiveSupport::Ractors.shareable_lambda でラップし、Ractor 環境でも安全に共有できるようにしました。機能的な変更はなく、既存コードの互換性を保ちつつマルチスレッド安全性を向上させた点が最大の意義です。