Redisリスナースレッドが死亡した場合に再起動するよう修正

rails/rails

Action Cable の Redis アダプタは、ブロードキャスト用に常駐スレッドを生成して接続を維持しますが、再接続試行が尽きたときにスレッドが終了すると、以降の subscribe がキューに詰め込まれたまま放置され、プロセスを再起動するまで放送が失われます。本PR はその状態を検知してスレッドを再生成し、再接続カウンタもリセットすることで、死んだリスナースレッドからの復旧を自動化します。

背景

Action Cable の Redis リスナースレッドは ensure_listener_running メソッドで @thread ||= Thread.new とメモ化されているため、一度生成されたスレッドが死んでも @threadnil にならず再生成されません。この挙動は #57690 で修正されたサブスクリプション数が 0 になるケースを防ぎますが、再接続試行回数が枯渇した場合 にはスレッドが終了したまま残り、以降の購読がすべて失われるという新たな障害を引き起こします。

技術的な変更

actioncable/lib/action_cable/subscription_adapter/redis.rbListener#ensure_listener_running に、死んだスレッドを検知してリセットするロジックが追加されました。具体的には以下のコードが挿入されています。

def ensure_listener_running
  # The internal sentinel subscription keeps the listener alive in
  # normal operation, but the thread can still die for other reasons
  # (e.g. exhausting the Redis reconnect attempts). Since this method
  # memoizes with `||=`, a dead thread would never be replaced and
  # every later subscribe would queue forever. Drop a dead thread so
  # the next subscribe spawns a fresh listener.
  if @thread && !@thread.alive?
    @thread = nil
    @reconnect_attempt = 0
  end

  @thread ||= Thread.new do
    Thread.current.abort_on_exception = true
    # … existing listener loop …
  end
end

このチェックは @thread が存在しかつ alive? が false のときに、スレッド参照を nil に戻し同時に @reconnect_attempt カウンタを 0 にリセットします。その後の @thread ||= Thread.new が新しいスレッドを生成できるようになります。コード変更は追加だけで既存ロジックを削除せず、既存の true/false 設定やサブスクリプションの動作に影響を与えません。

テスト側では actioncable/test/subscription_adapter/redis_test.rb にリスナースレッドが死んだ後に再起動することを検証するテストが追加されました。reconnect_attempts: 0 で即座に再接続を諦めさせ、drop_pubsub_connections 後にスレッドが死亡したことを wait_for ヘルパで確認し、続く新規チャンネル購読でスレッドが再生成されブロードキャストが正常に届くことをチェックします。

# 省略されたテスト本体は上記記事本文で示した通り

さらに、ポーリング用の wait_for ヘルパが actioncable/test/test_helper.rb に実装され、非同期状態の確認を固定スリープではなく条件が満たされるまで繰り返しチェックする形に改善されました。

def wait_for(message: "condition not met", timeout: 5, interval: 0.01)
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
  loop do
    return if yield
    if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
      raise Timeout::Error, "#{message} after #{timeout} seconds"
    end
    sleep interval
  end
end

設計判断

この変更は スレッドのリサイクルを防ぎつつ再生成を許容する という設計判断に基づきます。@thread のメモ化はパフォーマンス上有用ですが、死んだスレッドが再利用されるリスクを伴います。そこで、死んだスレッドを明示的に破棄しカウンタをリセット する最小限の分岐を導入することで、既存コードへの侵入度を抑えつつ信頼性を向上させました。代替案としては Thread.new を毎回呼び出す方式や、別キーでスレッド管理を行う案が考えられましたが、後者は大規模なリファクタリングが必要になるため採用されませんでした。

まとめ

ensure_listener_running に死んだスレッド検知とリセット処理を追加したことで、Redis 接続が再接続できずにスレッドが終了したケースでも自動的にリスナーが再起動します。これにより Action Cable のブロードキャストがプロセス再起動まで失われるリスクが除去され、既存の設定や動作に影響を与えない形で信頼性が向上しました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
90f5e9fa

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文があり、背景・技術的な変更・設計判断・まとめの各セクションが順序立てて配置されている。全ての必須要素が揃っている。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは `言語:ファイルパス` 形式で正しく記載され、PRリンクは `[#番号]` 形式でリンク化されている。

対象読者への適合性 ✓ PASS

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

専門的な技術用語と具体的な実装説明に留まり、初心者向けの余計な解説はない。

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

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

各セクションの冒頭が要旨となるトピックセンテンスで始まり、1段落1トピック、長さも適切で空行で区切られている。

Diff内容との照合 ✓ PASS

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

記事に掲載されたコードはDiffに示された追加内容と一致している。テストヘルパーの実装もDiffと合致している。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語は正確で、PR説明と整合している。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRのMotivationとDetailに基づき、事実に即した正確な記述となっている。

事実の突合 ✓ PASS

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

記事の全ての主張はPRのタイトル、Description、Diffで裏付けられており、外部知識や推測は含まれていない。

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

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

PR番号やファイルパスなどの数値・固有名詞は正確に記載されている。

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

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

記事タイトルはPRタイトル「Restart the Redis listener thread when it dies」の意味を日本語で適切に表現している。

外部知識の正確性 ✓ PASS

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

記事はPRに記載された情報以外の外部知識を付加していない。

時間表現の正確性 ✓ PASS

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

時間表現のずれはなく、PRの記述と一致している。