Action View テンプレートハンドラレジストリを Ractor 共有可能に

rails/rails

Action View のテンプレートハンドラレジストリをクラス変数からモジュールインスタンス変数へ移行し、Ractor 間で安全に共有できるようにしました。これにより、テンプレート検索やレンダリング時にレジストリへの書き込みが排除され、マルチ Ractor 環境でも安定して動作します。

背景

Ractor がマルチスレッドに代わる並行実行モデルとして利用される場所が増えているため、共有可能なデータ構造の整備が求められました。従来、ActionView::Template::Handlers@@template_handlers などのクラス変数でハンドラを保持しており、別スレッドや Ractor から参照した際に潜在的な書き込み競合が発生するリスクがありました。レジストリを不変にすることで、読み取りだけがロックフリーで行える設計へと転換する必要がありました。結果として、レジストリの状態管理をモジュールインスタンス変数へ移行し、全体を freeze した不変オブジェクトに置き換えました。

技術的な変更

actionview/lib/action_view/template/handlers.rb ではクラス変数を廃止し、モジュールレベルのインスタンス変数を初期化しています。

module Handlers # :nodoc:
  @template_handlers = {}.freeze
  @template_extensions = [].freeze
  @default_template_handler = nil

  class << self
    attr_reader :template_handlers, :default_template_handler

    def extensions
      @template_extensions
    end

    def register(*extensions, handler)
      handler = ActiveSupport::Ractors.try_shareable_proc(handler) if handler.is_a?(Proc)
      handlers = @template_handlers.dup
      extensions.each { |ext| handlers[ext.to_sym] = handler }
      @template_handlers = handlers.freeze
      @template_extensions = @template_handlers.keys.freeze
    end

    def register_default(extension, handler)
      register(extension, handler)
    end

    def unregister(*extensions)
      handlers = @template_handlers.dup
      extensions.each { |ext| handlers.delete(ext.to_sym) }
      @template_handlers = handlers.freeze
      @template_extensions = @template_handlers.keys.freeze
    end
  end
end

レジストリへの書き込みは コピーオンライトfreeze により不変化を保証し、register 呼び出し時に新しいハッシュを生成して置き換えます。handlerProc の場合は ActiveSupport::Ractors.try_shareable_proc で共有可能な形に変換し、ビルトインハンドラはモジュールロード時に直接登録されます。

ビルトインハンドラの登録は self.extended フックを削除し、ロード時に以下のように行われます。

Handlers.register_default :raw,    Raw.new
Handlers.register_default :erb,    ERB.new
Handlers.register_default :html,   Html.new
Handlers.register_default :builder, Builder.new
Handlers.register_default :ruby,   ActiveSupport::Ractors.shareable_lambda { |_, source| source }

actionview/lib/action_view/template/handlers/builder.rb では、ハンドラがフリーズされた状態でも安全に builder ライブラリをロードできるよう、キャッシュ用インスタンス変数を除去し defined? ガードに置き換えました。

class Builder # :nodoc:
  class_attribute :default_format, default: :xml

  def call(template, source)
    require "builder" unless defined?(::Builder)
    "xml = xml = ::Builder::XmlMarkup.new(indent: 2, target: output_buffer.raw);" \
      "#{source};" \
      "output_buffer.to_s"
  end
end

テストコード actionview/test/template/handlers_test.rb が新たに追加され、レジストリと拡張子リストが Ractor 共有可能であること、Ractor 内からハンドラが取得できること、そして Proc ハンドラ登録後もレジストリが共有可能であることを検証しています。

class TemplateHandlersTest < ActiveSupport::TestCase
  include ActiveSupport::Testing::Isolation
  include ActiveSupport::Testing::RactorsAssertions

  def test_built_in_handler_registry_is_ractor_shareable
    assert_ractor_shareable ActionView::Template::Handlers.template_handlers
    assert_ractor_shareable ActionView::Template::Handlers.extensions
  end

  def test_registering_a_proc_handler_keeps_the_registry_shareable
    ActiveSupport::Ractors.with(unshareable_proc_action: :raise) do
      ActionView::Template.register_template_handler :custom, lambda { |_, src| src }
      assert_ractor_shareable ActionView::Template::Handlers.template_handlers
    end
  ensure
    ActionView::Template.unregister_template_handler :custom
  end
end

これらの変更に伴い、actionview/test/template/render_test.rb でもレジストリ削除の手段が unregister_template_handler に置き換えられ、テストが不変レジストリに適合するよう修正されています。

設計判断

不変コピーオンライト + freeze の組み合わせは、Ractor が要求する「共有可能オブジェクトは不変」であるという前提と完全に合致します。ロックや同期機構を導入せずに読み取り専用にすることで、マルチ Ractor 環境でのオーバーヘッドを最小化しています。代替としては Concurrent::Map などのスレッド安全コレクションが考えられましたが、既存 API への影響を抑える点で今回の実装が選択されました。

ハンドラ自体を freeze しつつ、ActiveSupport::Ractors.shareable_lambdatry_shareable_proc を介して Proc オブジェクトを共有可能に変換する方針は、ユーザーが独自ハンドラを追加した際にも同様の安全性が保証される点がメリットです。これにより、アプリケーション側のコード変更は不要で、内部で自動的に共有化が行われます。

レジストリ状態管理を Handlers モジュールに集約し、self.extended フックを廃止したことは、初期化順序の曖昧さを解消し、ロード時に必ずハンドラが登録されることを保証します。結果として、テストや実行時に期待通りのハンドラが存在することが明確になり、コードベースの予測可能性が向上しました。

まとめ

この PR は Action View のテンプレートハンドラレジストリを不変かつ Ractor 共有可能な形に刷新し、クラス変数からモジュールインスタンス変数への移行、コピーオンライトと freeze による状態管理、そして共有可能な Proc 変換ロジックを導入しました。変更は既存の API にほとんど影響せず、マルチ Ractor 環境でも安全にハンドラ検索とテンプレートレンダリングが行えるようになっています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
029a101d

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的な変更・設計判断(任意)・まとめの5部構成が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトは正しい形式 (```ruby:filepath) で記述され、PRリンクは [#57874](URL) の形で正しくリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

対象は Rails エンジニアであり、専門用語と実装詳細が中心で初心者向けの余計な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論パラグラフ→各論パラグラフ→結論パラグラフで構成され、段落はトピックセンテンスで始まり 6 文未満に収められ、空行で区切られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは Diff の変更内容と一致しており、ファイル名・変更点・ロジックが正確に再現されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Ractor、shareable、freeze、copy‑on‑write などの用語が正しく使われ、PR の記述と一致しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的説明は PR の内容と整合し、論理的に根拠が示されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の全ての主張は PR タイトル、説明、Diff、テストコードで裏付けられており、推測や外部知識は含まれていません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #57874 だけが数値情報として使用され、正しく記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR タイトル「Make the Action View template handler registry Ractor‑shareable」の日本語訳として適切です。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

Rails のバージョンや LTS、リリース日程など PR に記載されていない外部情報は一切含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と一致しています。