はじめに
Rails 8の開発環境では、Active JobとAction Cableのデフォルトアダプターがいずれもasyncに設定されている。asyncアダプターはシングルプロセス・メモリベースの簡易的な仕組みであり、本番環境とは挙動が異なる。
Active Jobのretry_onでwaitオプションが効かないなど、非同期処理が意図通りに動作しない場合は、本番環境と同じSolid Queueへの切り替えが必要になる。しかし、Solid Queueを導入するとジョブが別プロセスで実行されるため、今度はTurbo Broadcastがブラウザに届かなくなる。これはAction Cableのasyncアダプターが同一プロセス内でしかメッセージを配信できないためである。解決するには、Action CableのアダプターもSolid Cableに変更する必要がある。
本記事では、この一連の問題の背景と設定手順をまとめる。
この記事を書いている人
fjord bootcampで2年1ヶ月プログラミングを学んでいます。現在、最後の個人開発のプラクティスでAIチャットアプリを作っています。
開発環境
- Rails 8
- Docker(PostgreSQL)
調べた背景
- Active Jobの
retry_onメソッドにwaitオプションを設定したが、指定した間隔で動作しなかった - Active Jobのアダプターを
asyncからSolid Queueに変更したところ、retry_onは正常動作するようになったが、Turbo Broadcastがブラウザに届かなくなった - Action Cableのアダプターを
asyncからSolid Cableに変更したところ、Turbo Broadcastが正常に届くようになった
基礎知識
Asyncアダプター(Active Job)
Rails 8のdevelopment環境におけるActive Jobのデフォルトアダプター。ジョブをメモリ上のスレッドプールで即座に実行する簡易的な仕組み。以下の制約がある。
retry_onのwait(遅延実行)が正常に動作しない- プロセスが終了すると未実行のジョブが失われる
- 同一プロセス内でのみ動作する
production環境ではデフォルトでSolid Queueが使用される。
Asyncアダプター(Action Cable)
Rails 8のdevelopment環境におけるAction Cableのデフォルトアダプター。broadcastされたメッセージを同一プロセス内の購読者にメモリ経由で配信する。
- 別プロセスからのbroadcastは受け取れない
- Solid Queueのジョブ内からのTurbo Broadcastはブラウザに届かない
※ Active JobのasyncとAction Cableのasyncは同じ名前だが別々の実装。たまたま同じ名前と制約(シングルプロセス・メモリベース)を持っている。
Solid Queue
Active Jobの本番用キューバックエンド。メモリの代わりにDBでジョブの予約・実行・リトライを管理する。遅延実行やリトライ戦略が正しく動作する。
Rails 8のデフォルトでは、Pumaのpluginとして起動される(plugin :solid_queue)。これにより別途プロセスを立てる手間を省いているが、内部的にはforkされた別プロセスとして動作する。ジョブの負荷が大きくなった場合は、bin/jobsで独立プロセスとして分離できる設計になっている。
Solid Cable
Action Cableのsubscriptionアダプター。メモリの代わりにDBでWebSocketメッセージの受け渡しを行う。DBを仲介するため、プロセスをまたいだbroadcastが可能になる。Solid Queueのジョブ内からのTurbo Broadcastもブラウザに届くようになる。
なぜ両方の変更が必要なのか
[変更前] async + async
Puma (HTTP + ActionCable + ActiveJob) ← 全て同一プロセス、broadcastは届く
[Solid Queueのみ導入]
Puma (HTTP + ActionCable) ←→ SolidQueue (ActiveJob) ← 別プロセス、broadcastが届かない
[両方導入] Solid Queue + Solid Cable
Puma (HTTP + ActionCable) ←→ DB ←→ SolidQueue (ActiveJob) ← DB経由でbroadcastが届く
設定手順
1. [Solid Queue] queue_adapterの設定
# config/environments/development.rb
# デフォルトのasyncからsolid_queueに変更
config.active_job.queue_adapter = :solid_queue
# Solid Queue用のDBを指定(database.ymlのqueueキーに対応)
config.solid_queue.connects_to = { database: { writing: :queue } }
2. [Solid Queue] Pumaプラグインの設定
# config/puma.rb
# development環境でもSolid QueueをPumaから起動する
# 本番ではSOLID_QUEUE_IN_PUMA環境変数で制御される(Kamalのdeploy.ymlで設定済み)
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] || Rails.env.development?
3. [Solid Cable] cable.ymlの設定
# config/cable.yml
development:
adapter: solid_cable # asyncからsolid_cableに変更
connects_to:
database:
writing: cable # database.ymlのcableキーに対応
polling_interval: 0.1.seconds # DBへのポーリング間隔
message_retention: 1.day # メッセージの保持期間
4. [Solid Queue / Solid Cable] database.ymlにDBを追加
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
username: monalin
password: password
host: db # Docker環境ではDBサービス名を指定(queue/cableのDB接続に必須)
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
primary:
<<: *default
database: monalin_development # 既存のアプリケーションDB
queue:
<<: *default
database: monalin_development_queue # Solid Queue用DB
migrations_paths: db/queue_migrate
cable:
<<: *default
database: monalin_development_cable # Solid Cable用DB
migrations_paths: db/cable_migrate
※
host: dbについて: queue/cableのDBはdatabase.ymlを直接参照するため、defaultセクションにhostの指定が必須。
5. テーブル作成
# サーバーを停止(DB接続を切断するため)
docker compose down
# queue/cableのDBとテーブルを作成
docker compose run --rm web bin/rails db:prepare
# サーバーを再起動
docker compose up
※
db:prepareはDBが存在しなければ作成してスキーマをロードし、既に存在すれば未実行のマイグレーションを実行する。db/queue_schema.rbとdb/cable_schema.rbがプロジェクトに存在することを事前に確認すること(Rails 8でrails newした場合は生成済み)。
参考リンク
- Active Job Basics - Rails Guide(日本語)
- Action Cable Overview - Rails Guide
- Solid Queue - GitHub
- Solid Cable - GitHub
- Rails Issue #53630 - asyncアダプターでbroadcastが届かない問題
Photo by Possessed Photography on Unsplash



