Ractor‑shareable TaggedLogging の導入

rails/rails

ActiveSupport::TaggedLogging に Ractor で安全に共有できるロガー生成メソッドが追加され、マルチ Ractor アプリケーションでも従来と同等のタグ付けロギングが利用可能になりました。この変更により、ActiveSupport::Ractors::Logger を内部に持つロガーを任意の Ractor から呼び出せるようになります。

背景

Ruby 4.0 で導入された Ractor はスレッドに代わる並行実行モデルとして期待されていますが、標準の Logger は内部でミューテックスを使用しているため、複数 Ractor から直接書き込みたときに安全性が保証されませんでした。Rails のロギングは ActiveSupport::TaggedLogging を介してタグ付けされることが前提となっており、Ractor 環境で同様の機能を提供する手段が欠如していました。この PR は、Ractor でもタグ付きロギングを継続利用できるようにすることを目的としています。

技術的な変更

ActiveSupport::Ractors 名前空間にロガー関連クラスを追加し、autoload による遅延ロードを設定しました。

module ActiveSupport
  # Shims for +Ractor+ shareability methods so framework code can call them
  # unconditionally regardless of the Ruby version.
  module Ractors # :nodoc:
    extend ActiveSupport::Autoload
    autoload :Logger

    class << self
      attr_accessor :unshareable_proc_action

ActiveSupport::Ractors::LoggerActiveSupport::Logger を継承し、DeviceProxy をログデバイスとして利用します。コンストラクタは従来の引数に加えて logdev_options を受け取り、バックグラウンドの Writer へ委譲します。

module ActiveSupport
  module Ractors # :nodoc:
    # A Ractor-shareable +ActiveSupport::Logger+.
    class Logger < ActiveSupport::Logger # :nodoc:
      def initialize(*args, level: ::Logger::DEBUG, progname: nil, formatter: nil, datetime_format: nil, **logdev_options)
        super(nil, level: level, progname: progname, formatter: formatter, datetime_format: datetime_format)
        @logdev = DeviceProxy.new(*args, **logdev_options)
      end

      delegate :flush, to: :@logdev

      extend ActiveSupport::Autoload
      autoload :DeviceProxy
      autoload :Writer
    end
  end
end

DeviceProxy は実際の書き込みを Writer の Ractor に非同期で送信し、write はバイト数を返すだけです。また flushclosereopen も適切に委譲します。

module ActiveSupport
  module Ractors # :nodoc:
    class Logger # :nodoc:
      class DeviceProxy # :nodoc:
        def initialize(*args, **logdev_options)
          @writer = Writer.spawn(*args, **logdev_options)
          @closed = false
        end

        def write(message)
          return if @closed
          @writer.async(message)
          message.bytesize
        end

        def flush
          @writer.flush unless @closed
          true
        end
        # ... (close, reopen omitted for brevity)
      end
    end
  end
end

WriterRactor::Port を用いてメッセージキューを構築し、async で書き込み要求を受け取り、call で同期的操作(flush, reopen など)を実行します。ポートが閉じられた場合は例外を捕捉し、呼び出し側にエラーを伝搬しません。

module ActiveSupport
  module Ractors # :nodoc:
    class Logger # :nodoc:
      class Writer # :nodoc:
        def self.spawn(...)
          new.spawn(...)
        end

        def initialize
          unless defined?(::Ractor::Port)
            raise NotImplementedError, "ActiveSupport::Ractors::Logger requires Ractor::Port support"
          end
          @port = ::Ractor::Port.new
        end
        # ... (spawn, async, call, flush, reopen omitted for brevity)
      end
    end
  end
end

最後に ActiveSupport::TaggedLogging.ractor_logger が公開インターフェースとして追加され、従来の logger と同様のシグネチャで呼び出せます。

class << self
  def logger(*args, **kwargs)
    new ActiveSupport::Logger.new(*args, **kwargs)
  end

  # Returns a logger that can be used from Ractors. Accepts the same arguments
  # as Logger.new. The returned logger is an ActiveSupport::Ractors::Logger
  # (a ::Logger subclass whose device proxies writes to a background Writer)
  # wrapped with tagged logging, so it can be made shareable and used from any
  # Ractor.
  def ractor_logger(*args, **kwargs) # :nodoc:
    new(ActiveSupport::Ractors::Logger.new(*args, **kwargs))
  end
end

設計判断

Ractor‑shareable ロガーは既存の ActiveSupport::Logger を継承し、内部だけを置き換える形で実装 されています。このアプローチにより、外部からは従来通り ::Logger のインターフェースを利用でき、既存コードとの互換性が保たれます。新しいクラスは :nodoc で隠蔽され、公開は ActiveSupport::TaggedLogging.ractor_logger のみとすることで API の肥大化を防いでいます。

また、autoload と DeviceProxy / Writer の分割により、Ractor が利用できない環境では NotImplementedError を早期に発生させ、起動時の失敗を防止しています。これにより、Ractor 非対応の Ruby バージョンでも安全にデプロイ可能です。さらに、flush を Rails のリクエストサイクルに合わせて呼び出す設計は、既存のロガー契約を尊重した上で非同期化を導入した点が特徴です。

まとめ

今回の PR は、ActiveSupport::TaggedLogging.ractor_logger というシンプルな入口を通じて、Ractor 環境でも安全にタグ付きロギングを利用できる基盤を提供します。内部実装は ActiveSupport::Ractors::Logger とその DeviceProxy/ Writer に分離され、非同期書き込みとリクエスト単位のフラッシュを保証。既存コードへの影響は最小限に抑えつつ、マルチ Ractor アプリケーションへの拡張性を大きく向上させました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
40f282b4

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

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

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい `ruby:ファイルパス` 形式で記述されていますが、PRへのリンクが `[PR #57684](URL)` となっており、要求された `[#57684](URL)` 形式になっていません。

対象読者への適合性 ✓ PASS

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

Rails のコア開発者向けの技術的内容で、余計な初心者向け解説はなく、対象読者に適しています。

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

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

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

Diff内容との照合 ⚠ WARNING

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

記事のコードブロックは Diff に含まれる内容を概ね反映していますが、`ractors/logger.rb` の冒頭の `# frozen_string_literal: true` や `require` 行、`DeviceProxy`・`Writer` の一部メソッド(close, reopen, spawn 以降等)が省略されています。省略は情報欠落とみなされ、完全な Diff との一致とは言えません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Ractor、TaggedLogging、DeviceProxy、Writer などの用語は PR で使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的説明は PR の内容と合致しており、根拠のない主張や誤情報はありません。

事実の突合 ✓ PASS

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

記事の全ての事実は PR のタイトル、説明、Diff から裏付けられています。外部知識の捏造は見られません。

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

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

数値・固有名詞(PR番号 #57684、Ruby 4.0 など)は正確です。

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

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

記事タイトルは PR の趣旨を適切に要約しており、一致しています。

外部知識の正確性 ✓ PASS

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

記事は PR に記載されている情報以外のバージョンサポートやリリース日程等の外部知識を追加していません。

時間表現の正確性 ✓ PASS

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

時間表現のずれはなく、PR の記述と整合しています。