ActiveSupport の JSON エンコーダを Ractor 共有可能にする変更
ActiveSupport の JSON エンコーダが Ractor で安全に共有できるようになり、従来必要だった Ractor.make_shareable の呼び出しが不要になりました。この変更は Ruby 4.0 以降でもテストがパスするように、エンコーダ内部のデータを不変化し、Ractor から直接利用できる形にしています。
背景
Ractor は Ruby の並列実行モデルであり、オブジェクトはデフォルトでは共有できません。そのため JSON エンコーダ を Ractor 内で利用する際は Ractor.make_shareable を明示的に呼び出す必要があり、コードが冗長になると同時にパフォーマンス上のオーバーヘッドも発生していました。PR の説明にあるように、テスト環境で Ruby 4.0 が導入された際にスキップが失敗する問題も報告されていました。エンコーダを事前に共有可能とすれば、Ractor 内での JSON エンコードがシンプルかつ高速になります。
技術的な変更
定数の不変化
activesupport/lib/active_support/json/encoding.rb では ESCAPED_CHARS の文字列リテラルが - 演算子でラップされ、暗黙的に freeze されるようになりました。さらに、正規表現定数 HTML_ENTITIES_REGEX、FULL_ESCAPE_REGEX、JS_SEPARATORS_REGEX が freeze 呼び出しで確実に不変化しています。
ESCAPED_CHARS = {
U2028 => -'\u2028'.b,
U2029 => -'\u2029'.b,
">".b => -'\u003e'.b,
"<".b => -'\u003c'.b,
"&".b => -'\u0026'.b,
}.freeze
HTML_ENTITIES_REGEX = Regexp.union(*(ESCAPED_CHARS.keys - [U2028, U2029])).freeze
FULL_ESCAPE_REGEX = Regexp.union(*ESCAPED_CHARS.keys).freeze
JS_SEPARATORS_REGEX = Regexp.union(U2028, U2029).freeze
エンコーダオプションの凍結
JSONGemEncoder のコンストラクタで受取った options が dup.freeze され、空のオプション {} も freeze された定数として保持されます。これによりエンコーダインスタンス自体が Ractor 共有可能 になります。
def initialize(options = nil)
@options = options.dup.freeze || {}.freeze
end
Coder の Proc を共有可能化
JSONGemCoderEncoder が利用可能な環境では、CODER が ActiveSupport::Ractors.shareable_proc でラップされたブロックで生成され、さらに freeze されています。shareable_proc は内部で Ractor.make_shareable を行うため、Ractor 間で安全に転送できます。
CODER = ::JSON::Coder.new(&ActiveSupport::Ractors.shareable_proc { |value, is_key|
# 既存ロジック省略
}).freeze
依存 gem のバージョン更新
Gemfile.lock で json gem が 2.15.2 から 2.19.9 に更新され、JSONGemCoderEncoder が利用できるバージョンが確保されています。バージョン更新はエンコーダの新機能と互換性を保つための必須変更です。
- json (2.15.2)
+ json (2.19.9)
設計判断
既存キーの拡張 という方針で実装が進められました。新たな設定項目やクラスを追加せず、既存のエンコーダクラスや定数に対して不変化処理と shareable_proc ラップを施すことで、後方互換性を維持しながら Ractor 共有を実現しています。- 演算子による文字列凍結は Ruby の標準的な不変化手段であり、追加の依存やランタイムチェックを避けられます。また、CODER の生成を一箇所に集約し freeze することで、オブジェクトのコピーコストを最小化しつつ安全な共有を保証しています。これらの選択は「最小限の侵入で Ractor 対応を提供」するという設計目標に沿ったものです。
まとめ
この PR により、ActiveSupport の JSON エンコーダは Ractor から直接呼び出せる Ractor‑shareable なオブジェクトとなり、Ractor.make_shareable を明示的に呼び出す必要がなくなります。定数の凍結、オプションの不変化、shareable_proc の活用というシンプルな改修で、既存コードの動作は変わらず、Ruby 4.0 以降でもテストが通過するようになりました。