Action Cable のサーバー層アダプタ化:低レベル処理とアプリケーション層の分離

rails/rails

Action Cable の内部アーキテクチャが大幅に再編され、WebSocket処理や並行制御などの低レベル実装と、ChannelやConnectionといったアプリケーション層の抽象が明確に分離されました。これにより、ユーザーコードを変更せずに代替サーバー実装や実行モデルを利用できる拡張性の基盤が整います。

背景

これまでの Action Cable では、ActionCable::Connection::Base が WebSocket イベントリスナーの設定、Rails Executor のラッピング、ユーザーコードの実行という異なる責務を一手に担っていました。この密結合により、非WebSocket環境やユニットテストでの再利用が困難であり、代替サーバー(Iodine、SSEなど)との統合には内部へのモンキーパッチが必要でした。同様の問題意識は rails/rails#27648 でも取り上げられており、本PRはその目標を改めて実現するものです。

結果として、ActionCableをライブラリとして他の実行モデル(Fiberベースの並行処理など)から活用することが現実的ではありませんでした。

技術的な変更

Connection::Base と Server::Socket の分割

最も大きな変更は、ActionCable::Connection::Base からWebSocket固有の処理を ActionCable::Server::Socket として切り出したことです。Server::Socket がWebSocket接続の低レベル処理(エンコード/デコード、メッセージバッファリング、プロトコルハンドシェイク)を担い、Connection::Base はアプリケーション層の純粋な抽象として残ります。

変更前:

def initialize(server, env, coder: ActiveSupport::JSON)
  @server, @env, @coder = server, env, coder
  @worker_pool = server.worker_pool
  @logger = new_tagged_logger
  @websocket      = ActionCable::Connection::WebSocket.new(env, self, event_loop)
  @subscriptions  = ActionCable::Connection::Subscriptions.new(self)
  @message_buffer = ActionCable::Connection::MessageBuffer.new(self)
  ...
end

変更後:

def initialize(server, socket)
  @server = server
  @socket = socket
  @logger = socket.logger
  @subscriptions  = Subscriptions.new(self)
  ...
end

Connection::Basesocket オブジェクトへの委譲(envrequestprotocolperform_work)と server への委譲(pubsubexecutorconfig)を通じて動作します。サーバー側では call メソッドが直接 Socket.new を呼び出すよう変更されています。

def call(env)
  return config.health_check_application.call(env) if env["PATH_INFO"] == config.health_check_path
  setup_heartbeat_timer
  Socket.new(self, env).process  # 変更前: config.connection_class.call.new(self, env).process
end

ThreadedExecutor の独立

ActionCable::Server::ThreadedExecutor が新設され、これまで StreamEventLoop 内に隠蔽されていた Concurrent::ThreadPoolExecutor が独立したサーバー属性として昇格しました。インターフェースは #post#timer#shutdown の3メソッドのみに絞られています。

class ThreadedExecutor
  def initialize(max_size: 10, name: "server")
    @executor = Concurrent::ThreadPoolExecutor.new(
      name: "ActionCable-#{name}",
      min_threads: 1,
      max_threads: max_size,
      max_queue: 0,
    )
  end

  def post(task = nil, &block) = @executor << (task || block)
  def timer(interval, &block) = Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
  def shutdown = @executor.shutdown
end

これに伴い Configurationexecutor_pool_size(デフォルト10)が追加されました。また StreamEventLoop から timer/post メソッドが削除され、IO監視専用の役割に整理されています。

SubscriberMap::Async の標準化

非同期pub/sub処理 がアダプター層で統一されました。以前は各アダプター(Async、PostgreSQL、Redis)がそれぞれ独自に event_loop.post を呼び出していましたが、SubscriberMap::Async サブクラスに集約されました。

class Async < self
  def initialize(executor)
    @executor = executor
    super()
  end

  def add_subscriber(*) = @executor.post { super }
  def remove_subscriber(*) = @executor.post { super }
  def invoke_callback(*) = @executor.post { super }
end

これにより stream_from での二重非同期呼び出し(Channelレイヤーの event_loop.post + アダプター内の非同期処理)が解消されました。

ファイル移動と削除

低レベル実装クラスが ActionCable::Connection 名前空間から ActionCable::Server::Socket 配下に移動されました。

変更前 変更後
Connection::ClientSocket Server::Socket::ClientSocket
Connection::WebSocket Server::Socket::WebSocket
Connection::Stream Server::Socket::Stream
Connection::MessageBuffer Server::Socket::MessageBuffer
Connection::StreamEventLoop Server::StreamEventLoop
Connection::TaggedLoggerProxy Server::TaggedLoggerProxy

Subscriptions のエラーハンドリング改善

Subscriptions クラスに専用の例外クラス群が追加されました。以前はエラーログの書き出しにとどまっていた異常系が、呼び出し元でハンドリングできる例外として整理されています。

追加された例外クラスは以下のとおりです:

  • AlreadySubscribedError — 同一チャンネルへの重複サブスクライブ
  • ChannelNotFound — 存在しないチャンネルID
  • MalformedCommandError — 不正なコマンドデータ
  • UnknownCommandError — 未知のコマンド
  • UnknownSubscription — 該当するサブスクリプションが存在しない

テストインフラの改善

テスト用の ConnectionStub/ChannelStub が廃止され、実際のクラスを使用するアーキテクチャに移行されました。TestSocket クラスが新設され、テスト環境でも本番と同じ Connection::BaseChannel クラスがそのまま動作します。advance_time ヘルパーも追加され、定期タイマーのテストが可能になっています。また、各サブスクリプションアダプターが @server.mutex ではなく独自の @mutex を持つように変更されており、サーバーオブジェクトへの依存が軽減されています。

設計判断

後方互換性を保ちながら内部アーキテクチャを刷新する という方針が一貫して採られています。PRは「本PRはパブリックAPIの破壊的変更を含まない」と明示しており、ユーザー向けの ApplicationCable::ConnectionApplicationCable::Channel の記述方法は変わりません。

_Socket_Executor_PubSub_Connection といったインターフェース(PRのクラス図参照)を定義することで、将来の代替実装が準拠すべき契約を明確にしています。ThreadedExecutor の最小インターフェース(#post/#timer/#shutdown)もこの思想を体現しており、Fiberベースのエグゼキュータへの差し替えなどを将来のPRで導入しやすくしています。ただし、config.action_cable.async_executor のような設定キーは本PRには含まれておらず、フォローアップに委ねられています。

各アダプターが @server.mutex を共用していた点の改善も注目に値します。InlinePostgreSQLRedis の各アダプターがそれぞれ独自の @mutex を持つようになったことで、サーバーオブジェクトへの不必要な依存が排除されています。これはアダプター単体での利用可能性を高める変更です。

まとめ

本PRは、Action Cable の「WebSocket処理とアプリケーションロジックの分離」という長年の課題(rails/rails#27648 からの継続)に対する実質的な回答です。Server::SocketThreadedExecutorSubscriberMap::Async という3つの明確な抽象化を導入することで、既存ユーザーコードへの影響を最小化しつつ、Iodineのネイティブpub/sub活用やSSEトランスポートの実装といった代替実装の土台が整いました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
ed34c7f0

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)」という3部構成が明確に適用されており、記事の骨子が非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```ruby:path/to/file.rb)やGitHubのPR番号へのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Action Cableの内部アーキテクチャという専門的なトピックを扱っており、専門知識を持つエンジニアという対象読者に適した技術レベルで記述されています。

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

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

各セクションが総論→各論の構成で書かれ、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiffの内容とファイル名を正確に反映しています。変更の前後関係が分かりやすく示されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ThreadedExecutor」「SubscriberMap」「pub/sub」などの技術用語や、Action Cable固有のクラス名が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

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

「Connection::Baseからの責務分離」や「二重非同期呼び出しの解消」といった技術的な説明は、PR DescriptionやDiffのコード変更によって裏付けられており、論理的で正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張がPRのDescriptionやDiffの内容に基づいており、根拠のない推測や憶測(ハルシネーション)は見られません。

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

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

PR番号(#50979)、参照PR番号(#27648)、設定値(executor_pool_size: 10)など、記事に含まれる数値や固有名詞はすべて正確です。

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

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

「Action Cable のサーバー層アダプタ化」というタイトルは、PRの主題である「Action Cable server adapterization」を的確に翻訳し、内容を正確に要約しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に完全に準拠しており、PRに記載のない外部知識(バージョンのサポート状況など)の追記はありません。

時間表現の正確性 ✓ PASS

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

「これまでの」「将来の」といった時間に関する表現は、PRの文脈(既存の問題と将来の拡張性)と一致しており、正確です。