default_scope を Ractor セーフにする変更

rails/rails

ActiveRecord の default_scope が Ractor 環境で安全に共有できるよう、スコープオブジェクトをデフォルトで凍結し、プロックを共有可能に変換する処理が追加されました。これにより、マルチスレッド・マルチプロセス環境でのデフォルトスコープ利用時にデータ競合やコピーコストが排除されます。

背景

default_scope は内部でプロックを保持し、クエリ生成時に instance_exec で評価されますが、Ractor 間でそのプロックが非共有(unshareable)であると例外が発生します。Rails 7 以降で Ractor の利用が推奨される中、デフォルトスコープが原因でアプリケーションが起動できないケースが報告されました。今回の PR はその問題を根本的に解決し、Ractor との互換性を確保することを目的としています。

技術的な変更

DefaultScope のコンストラクタでプロックを ActiveSupport::Ractors.try_shareable_proc にラップし、生成されたインスタンスを freeze しています。これにより、作成時点でオブジェクトが不変となり、Ractor 間で安全に共有できます。

class DefaultScope # :nodoc:
  attr_reader :scope, :all_queries

  def initialize(scope, all_queries = nil)
    @scope = ActiveSupport::Ractors.try_shareable_proc(scope)
    @all_queries = all_queries
    freeze
  end
end

クラス属性 default_scopes のデフォルト値を [].freeze に変更し、配列自体も不変にしました。これにより、スコープが追加されるたびに新しい配列を生成し、古い配列は変更不能となります。

included do
  # Stores the default scope for the class.
  class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: [].freeze
end

default_scope メソッドの実装を更新し、self.default_scopes = [*default_scopes, default_scope].freeze という形で配列を再構築して凍結しています。従来のミュータブルな += 操作を排除し、すべてのスコープ集合が共有可能であることを保証します。

def default_scope(scope = nil, all_queries: nil, &block) # :doc:
  if scope
    default_scope = DefaultScope.new(scope, all_queries)
    self.default_scopes = [*default_scopes, default_scope].freeze
  end
  ...
end

テスト側では ActiveSupport::Testing::RactorsAssertions を導入し、assert_ractor_shareable マクロで default_scopes が Ractor 共有可能であることを検証しています。新規テストは Ractor コンテキスト内でモデルを定義し、default_scope が正しく評価されることも合わせて確認します。

require "active_support/testing/ractors_assertions"
require "active_support/core_ext/object/with"

class DefaultScopingTest < ActiveRecord::TestCase
  include ActiveSupport::Testing::RactorsAssertions
  ...
  def test_default_scopes_are_ractor_shareable
    ActiveSupport::Ractors.with(unshareable_proc_action: :raise) do
      model = Class.new(ActiveRecord::Base) do
        def self.name = "ractor_safe_posts"
        self.table_name = "posts"

        default_scope -> { ractor_safe }

        def self.ractor_safe
          where(type: "ractor_safe")
        end
      end

      select_sql = capture_sql { model.all.to_a }.first
      assert_match(/type/, select_sql)
      assert_ractor_shareable model.default_scopes
    end
  end
end

設計判断

本変更は 「不変オブジェクト化」「プロックの共有可能化」 という二本柱で実装されています。freeze による不変化は既存の API 挙動を変えず、単に内部データ構造の安全性を高めるだけで済みます。また、try_shareable_proc を使用することで、非共有プロックが渡された場合は例外を即座に通知し、開発者が早期に問題を検出できるようにしています。配列操作をミュータブルからイミュータブルに切り替える設計は、Ractor が前提となる環境でのデータ競合を根本的に防止するための最小侵襲なアプローチです。

まとめ

default_scope が Ractor 安全になることで、Rails アプリケーションはマルチプロセス実行時にスコープ定義の共有が保証され、予期しない例外やパフォーマンス低下を回避できます。変更は不変化と共有可能プロックの導入に絞られ、既存コードへの影響は最小限に抑えられています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
343ae494

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断、まとめの全セクションが揃っており、総論→各論→結論の構成が明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい `language:filepath` 形式ですが、PRリンクが `[PR #57918](URL)` となっており、要求された `[#57918](URL)` 形式から若干逸脱しています。

対象読者への適合性 ✓ PASS

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

対象はRailsエンジニアであり、専門用語中心の記述で初心者向けの余分な説明はありません。

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

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

各セクションは総論パラグラフ→具体的説明→結論パラグラフで構成され、段落はトピックセンテンスで始まり、長さも適切です。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語はPRで示されたものと一致しており、誤用はありません。

説明の技術的正確性 ✓ PASS

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

変更理由や実装内容の説明はPRの記述と合致し、技術的に正確です。

事実の突合 ✓ PASS

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

すべての主張がPR情報またはDiffで裏付けられており、根拠のない推測はありません。

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

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

PR番号 #57918 などの数値・固有名詞は正確です。

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

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

記事タイトルはPRの「Make default scopes ractor safe」の趣旨と一致しています。

外部知識の正確性 ✓ PASS

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

LTSやリリース日程等の外部情報は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現は使用されておらず、PR内容と矛盾する点はありません。