Redis アダプタの依存を明示化してロード競合を防止
Action Cable の Redis サブスクリプションアダプタが暗黙的に参照していた Base と ChannelPrefix をファイル冒頭で明示的に require することで、Zeitwerk の遅延ロード時に起きていたレースコンディションを根本的に解消します。この変更により、Puma クラスターモード下でもブロードキャストが安定して実行できるようになります。
背景
ActionCable::SubscriptionAdapter::Redis は Base クラスと ChannelPrefix モジュールを同ファイル内で直接参照しますが、subscription_adapter/ ディレクトリは loader.do_not_eager_load により eager load から除外されています。そのため初回参照時に Zeitwerk が遅延ロードで定数を解決しようとし、複数スレッドが同時にロードを要求すると NameError: uninitialized constant ActionCable::SubscriptionAdapter::Base が発生します。この例外はプロセスの生存期間中に状態が保持され、Redis アダプタが未定義のまま残るため、以降のすべてのブロードキャストが失敗し続けます。
観測された実害として、Rails 7.2.3.1 アプリケーションが Puma クラスターで稼働中に約 500 件のブロードキャストが失われ、15 件の Sentry イベントが 80 分間にわたって蓄積された事例があります。これらはすべて、暗黙的依存がスレッドセーフでないことに起因する典型的な失敗パターンです。したがって、確実にロード順序を保証できる手段が求められました。
技術的な変更
本 PR では action_cable/subscription_adapter/redis.rb の冒頭に 2 行の require 文を追加し、兄弟定数への依存を明示化しました。
require "active_support/core_ext/hash/except"
require "action_cable/subscription_adapter/base"
require "action_cable/subscription_adapter/channel_prefix"
module ActionCable
module SubscriptionAdapter
class Redis < Base # :nodoc:
追加前は Base と ChannelPrefix が暗黙的にロードされていたため、ファイル評価時点で定数が未定義になるリスクが残っていました。require を行うことでファイルが読み込まれる瞬間に必要クラスが確実にロードされ、Zeitwerk の遅延ロードウィンドウが介在しなくなります。この変更は既存のファイルスタイル(redis.rb がすでに redis 本体と active_support/core_ext/hash/except を明示的に require している)と整合しており、コードベース全体の一貫性を保ちます。
設計判断
依存を明示的に require する 方針が採用された理由は、暗黙的 autoload がスレッド競合時に未初期化定数を引き起こすという実証されたリスクを回避するためです。明示的 require によりロード順序が保証され、Zeitwerk の内部ロックに依存しない安全な初期化が実現します。また、既存の require パターンに合わせて実装したため、後方互換性が維持され、既存コードが挙動を変えることはありません。この設計は「Adapters are required and loaded on demand」という action_cable.rb のコメント意図と完全に合致しています。
まとめ
Redis アダプタが自らの依存を明示的に require することで、Zeitwerk の遅延ロードに起因するレースコンディションを根本的に排除しました。結果として Puma クラスターモード下でもブロードキャストが安定し、同様のパターンを持つ他アダプタへの拡張も容易になる設計指針が示されました。