Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rails Action Cable徹底解説(Solid Cable対応):Redisなしでリアルタイムアプリを構築する方法

Webアプリケーションにおいて、リアルタイム機能の重要性はますます高まっています。しかし、Railsに組み込まれているWebSocketライブラリ「Action Cable」を熟知している開発者は、実はそれほど多くありません。

RailsのAction Cableは以前からWebSocketをサポートしてきましたが、その分だけ追加の複雑さも伴っていました。Rails 8では、新たにSolid Cableが導入されました。これはAction Cable向けの新しいデータベースバックエンドアダプタで、Redisが不要になります。このガイドでは、Solid CableとともにAction Cableの使い方を解説し、実際にリアルタイム機能を構築する方法をご紹介します。Redisの手間なく、Rails 8アプリにリアルタイム機能を簡単に追加できることがわかるでしょう。

ぜひ記事を読み進めながら一緒にアプリを構築してみてください。完成したプロジェクトはGitHubでも公開されているので、そちらを参照するのもOKです。

なぜRails Action Cableを使うのか?

モダンなWebアプリでは、更新情報をリアルタイムにクライアントへ配信するニーズがよくあります。たとえば、チャットメッセージが即座に表示されるケースや、ライブダッシュボードの通知などがわかりやすい例です。Action Cableは、WebSocketをアプリに統合するためのRails標準のソリューションであり、サーバーとクライアント間の双方向・永続的な通信を実現します。筆者自身、Action CableがRailsフレームワークの一部として提供されていることに感謝しています。「本当に役立つWebアプリを作るために必要なものはすべて揃っている」というRailsの思想を、Action Cableも支えているからです。Action Cableを使えば、ブラウザからの明示的な要求(ユーザーによる再読み込みなど)なしに、サーバーからブラウザへデータを送信できます。

Action CableとWebSocketを活用すれば、従来のサーバーレンダリング型アプリでは実装が難しかった、ライブでインタラクティブな機能をRailsアプリに組み込めます。日常的なユースケースとしては、次のようなものが挙げられます。

  • ライブチャットアプリケーション
  • 通知やフィード
  • リアルタイム同期型のコラボレーションアプリ
  • スポーツ中継や株価ティッカー

要するに、Action Cableは従来のリクエスト/レスポンスサイクルと、リアルタイムのイベント駆動型更新との間のギャップを埋めてくれる仕組みです。クライアント側では、Railsが提供するJavaScriptコンシューマーを使ってチャンネルを購読し、ブロードキャストを受信します。開発者は、コントローラに似た「チャンネル」(リアルタイムストリーム用)をバックエンドで定義し、フロントエンドのクライアントに購読させます。

Solid Cableとは何か?

以前のRailsバージョンでAction Cableを使ったことがある方なら、本番環境では通常Redis(またはPostgreSQLのNOTIFY)に依存して、複数のサーバープロセス間でメッセージをブロードキャストしていたことをご存じかもしれません。Pub/Subサービス(多くの場合Redis)は、あるRailsプロセスで発生したメッセージを他のすべてのプロセスに届け、それぞれが接続中のWebSocketクライアントへ転送できるようにする役割を担っています。この追加インフラは、これまでAction Cableを使う上で事実上必須でした。

Rails 8で導入されたSolid Cableは、既存のデータベースをバックエンドとして使うことで、Redisのような外部Pub/Subサービスを不要にします。Solid Cableは、Active Jobに対するSolid Queueや、キャッシュに対するSolid Cacheと同様に、Action Cableのためのデータベースバックエンドアダプタです。受信したWebSocketメッセージはすべてデータベースのテーブルに書き込まれ、すべてのAction Cableインスタンスがそのテーブルをポーリングして、新しいメッセージをクライアントへブロードキャストします。この処理は非常に高速で(デフォルトでは100ミリ秒ごと)、ほぼリアルタイムのパフォーマンスを実現します。メッセージは短時間のみ保存され(デフォルトでは24時間)、その後削除されるため、最近の問題をデバッグしつつ、データベース容量を気にする必要もありません。

全体として、Solid CableはRails 8の「Solidトリフェクタ」という思想に沿ったものです。これは、キャッシュ、バックグラウンドジョブ、リアルタイムメッセージングをカバーする、データベースバックエンドの組み込み機能一式です。Solid Cableによって、ジョブ処理・キャッシュ・WebSocketのすべてをデータベースだけで運用できる最後のピースが揃いました。

Solid Cableを使ったRails 8アプリの構築

そろそろSolid Cableを実際に触ってみたくなってきたのではないでしょうか。ここからは、Rails 8アプリケーションにSolid Cableを追加し、複数のユーザーがリアルタイムでメッセージをやり取りできる最小限のチャットルームを構築していきます。

サンプルアプリの作成

このチュートリアルでは、Solid Cableをバックエンドとして使いながら、Action Cableの基本要素(チャンネル、サブスクリプション、ブロードキャスト)を学びます。Rails 8を使用するので、まず次のコマンドで新しいRailsアプリを作成しましょう。

rails _8.1.0_ new solid_cable_chat --database=sqlite3

次に、作成されたsolid_cable_chatディレクトリにcdで移動します。

Rails 8を使っている場合、Solid Cableやその他のgemを追加する必要はありません。ほとんど(またはすべて)の設定は最初から用意されています。古いバージョンのRailsから移行してくる方のために、ここでは設定内容を一通り解説します。

Solid Cableの設定

まず、Solid Cableのセットアップコマンドを実行します。

bin/rails solid_cable:install

このジェネレータは主に2つのことを行います。1つ目は、Solid Cableをケーブルアダプタとして設定するconfig/cable.ymlファイルの生成です。2つ目は、Solid Cableのmessagesテーブルのスキーマ定義を含むdb/cable_schema.rbファイルの作成です。なお、最近のRailsバージョンでは、rails newの実行時にこれらのファイルが自動的に作成されます。

続いて、Solid Cable用のデータベース設定を行います。デフォルトでは、Railsはリアルタイムメッセージングのデータを他のデータから分離するため、Solid Cable専用のデータベースを使用します。開発環境では、同じデータベースを使うことも、別のデータベースを用意することも可能です。ここでは、開発環境でSolid Cable用に独立したSQLiteデータベースを使う方法を紹介します。そのために、新しい「cable」データベース接続を追加します。

Solid Cable用データベースのセットアップ

config/database.ymlファイルを開き、developmentセクションにcableデータベースを追加します。SQLite(Railsの開発環境のデフォルト)を使っている場合は、次のようになります。

development:
 primary:
 <<: *default
 database: storage/development.sqlite3
 cable:
 <<: *default
 database: storage/development_cable.sqlite3
 migrations_paths: db/cable_migrate

production:
 primary:
 <<: *default
 database: storage/production.sqlite3
 cache:
 <<: *default
 database: storage/production_cache.sqlite3
 migrations_paths: db/cache_migrate
 queue:
 <<: *default
 database: storage/production_queue.sqlite3
 migrations_paths: db/queue_migrate
 cable:
 <<: *default
 database: storage/production_cable.sqlite3
 migrations_paths: db/cable_migrate

繰り返しになりますが、十分に新しいバージョンのRailsを使っていれば、この設定はすでに存在しているはずです。

次に、config/cable.ymlを開きます。本番環境では、Solid Cableがすでにデフォルトアダプタになっているはずです。今回は開発環境でもSolid Cableを有効化したいので(localhostでチャットをテストできるようにするため)、developmentのアダプタをsolid_cableに変更し、先ほど設定したcableデータベースを指すように編集します。

development:
 adapter: solid_cable
 connects_to:
 database:
 writing: cable
 polling_interval: 0.1.seconds
 message_retention: 1.day

test:
 adapter: test

production:
 adapter: solid_cable
 connects_to:
 database:
 writing: cable
 polling_interval: 0.1.seconds
 message_retention: 1.day

上記のcable.ymlでは、developmentのアダプタをsolid_cableに設定し、productionの設定値をコピーしました。connects_toの設定により、Action Cableは(database.ymlで定義された)cableデータベースをメッセージ保存先として使用します。この変更は、最新バージョンのRailsを使っている場合でも必要です。

小規模なアプリでは、プライマリデータベースにSolid Cableのテーブルを持たせることも可能です(スキーマをマイグレーションにコピーし、個別のDB設定を削除します)。ただし、メインのアプリデータとのパフォーマンス干渉を避けるため、独立したデータベースの使用が推奨されます。

最後に、rails db:prepareを実行してデータベースの準備を整えましょう。アプリを本番環境にデプロイする場合も、同様にこのコマンドが必要です。

Action Cableチャンネルの設定

Action Cableは「チャンネル」を通じて動作します。チャンネルとは、データのストリームを扱うRubyクラスで、HTTPリクエストを処理するコントローラにやや似ています。チャット機能用のチャンネルを作成しましょう。名前はUserChatChannelとします。ジェネレータを使います。

rails generate channel UserChat

生成されたapp/channels/user_chat_channel.rbを開き、新しいロジックを追記します。

クライアントがUserChatChannelを購読すると(チャットページを開くとき)、subscribedコールバックが呼び出されます。このコールバック内でstream_from "user_chat_channel"を呼び出し、"user_chat_channel"という名前のブロードキャストからのストリーミングを開始します。

要するに、「user_chat_channelストリームにブロードキャストされたデータを待ち受け、このチャンネルのクライアントに渡す」という意味になります。このチャンネルを購読している全ユーザーは、"user_chat_channel"へのメッセージブロードキャストを受け取ります。

さらに、カスタムアクションも定義します。ここではtalk(data)と呼ぶことにします。チャンネル内のpublicメソッドは、クライアント側から呼び出せます。クライアントがperform("talk", { content: "Hello World" })を呼び出すと、サーバー側でtalkメソッドが実行されるわけです。

talkの実装では、クライアントから送られたメッセージ本文を取り出し、ActionCable.server.broadcastを使って"user_chat_channel"を購読している全員に送信します。つまり、送信者を含むすべての購読者が、メッセージデータをリアルタイムに受け取ります。ここでは単純にメッセージ本文を含むハッシュをブロードキャストしていますが、必要に応じてユーザー名やタイムスタンプなどの情報を含めることもできます。注意: 実際のアプリでは、ここでメッセージをデータベースに保存したり、バリデーションを行ったりすることになるでしょう。今回はシンプルさを優先して、ブロードキャストのみ行います。

class UserChatChannel < ApplicationCable::Channel
 def subscribed
 stream_from "user_chat_channel"
 end

 def unsubscribed
 # 購読解除時に必要な後処理があればここに書く
 end

 def talk(data)
 message = data["content"]
 ActionCable.server.broadcast("user_chat_channel", { content: message })
 end
end

クライアント側のコンシューマー構築

バックエンドができあがったので、次はフロントエンドをつなぎ込み、ユーザーがWebSocket経由でメッセージを送受信できるようにして、リアルタイム機能をデモできるようにしましょう。

Rails 8には、Action CableのJavaScript関連が最初から組み込まれています。ジェネレータがapp/javascript/channels/user_chat_channel.jsというファイルを作成してくれたので、ここにクライアントの振る舞いを実装していきます。

app/javascript/channels/user_chat_channel.jsを開き、次のように編集してください。

import consumer from "channels/consumer";

const userChatChannel = consumer.subscriptions.create("UserChatChannel", {
 connected() {
 console.log("Connected to UserChatChannel.");
 },

 disconnected() {
 console.log("Disconnected from UserChatChannel.");
 },

 received(data) {
 const messagesDiv = document.getElementById("messages");
 if (messagesDiv && data.content) {
 const messageElement = document.createElement("p");
 messageElement.textContent = data.content;
 messagesDiv.appendChild(messageElement);
 }
 }
});

function sendMessage(content) {
 userChatChannel.perform("talk", { content: content });
}

export { sendMessage };
window.sendMessage = sendMessage;

ここでは、consumer.subscriptions.create("UserChatChannel", {...})を使って、サーバー上のUserChatChannelへのサブスクリプションを作成しています。戻り値のサブスクリプションオブジェクトを通じて、チャンネルとやり取りできます。

connected()コールバックは、接続が確立されたときに実行されます。ここでは動作確認のため、コンソールにログを出力するだけにしています。

disconnected()コールバックは、WebSocketが切断された場合に実行されます。

received(data)コールバックが重要です! このコールバックは、サーバーからのブロードキャストをチャンネルが受信するたびに発火します。UserChatChannel#talkでは{ content: message }をブロードキャストしているため、ここのdata引数には同じハッシュが渡されます。これにより、新しいメッセージが届いた瞬間に、接続中の全クライアントのチャットログが即座に更新されます。

さらに、userChatChannel.perform("talk", { content: ... })を呼び出すヘルパー関数sendMessage(content)も定義しています。これは、ユーザーが入力したメッセージ本文を添えて、サーバー側で定義したtalkアクションへのリクエストを送るものです。

次に、ユーザーがメッセージを送受信するためのシンプルなUIが必要です。非常に基本的なビューを作成しましょう。

サンプルアプリ用のシンプルなUI構築

まず、コントローラを生成します。

rails generate controller UserChat index

次に、indexビューを開き、基本となるマークアップを記述します。

<h1>Chats from Users</h1>

<div id="messages" style="border: 1px solid #ccc; padding: 1em; height: 200px; overflow-y: auto; margin-bottom: 1em;">
 <!-- メッセージがここに表示されます -->
</div>

<form id="chat-form" onsubmit="event.preventDefault(); sendMessage(document.getElementById('chat-input').value); document.getElementById('chat-input').value = '';">
 <input type="text" id="chat-input" placeholder="Type a message..." autocomplete="off" style="width: 80%;" />
 <button type="submit">Send</button>
</form>

最後に、config/routes.rbでルートをこの新しいページに向けて設定します。

root "user_chat#index"

全体の動作確認

シンプルなチャットアプリの準備が整いました! bin/devでプロジェクトを起動し、localhost:3000にアクセスしてください。

Rails Action Cable徹底解説(Solid Cable対応):Redisなしでリアルタイムアプリを構築する方法

リアルタイム更新を確認するには、アプリを2つの異なるブラウザタブで開きます。片方のタブで「Hello from tab number 1!」のようなメッセージを入力してみましょう。

Rails Action Cable徹底解説(Solid Cable対応):Redisなしでリアルタイムアプリを構築する方法 もう一方のタブからメッセージを送ると、最初のタブにもそのメッセージが表示されるはずです!

Rails Action Cable徹底解説(Solid Cable対応):Redisなしでリアルタイムアプリを構築する方法

Rails Action Cableの本番環境へのデプロイ

Solid CableはWebSocketメッセージをデータベーステーブルに保存します。前述の例ではデフォルトのcableデータベースを使用しました。Rails 8の新規アプリでは、Solid CableにSQLiteがデフォルトで使われますが、技術的にはconfig/database.ymlcableセクションを追加することで、Railsがサポートする任意のデータベースを指定できます。

実際、本番環境ではSolid Cable専用のデータベースを使用することが推奨されています。これにより、リアルタイムメッセージングの負荷を他のデータから分離できます。たとえば、プライマリのアプリデータをapp_productionに置きつつ、Solid Cable専用にapp_production_cableデータベースを用意するといった具合です。

この分離により、チャットや通知のトラフィックが、メインアプリケーションのクエリと競合することを防げます。とはいえ、小規模なアプリであれば、アプリデータとCableメッセージの両方に1つのデータベースを使っても通常は問題ありません。

見落としがちなポイントは、デプロイ環境にSolid Cable用データベースを確実に組み込むことです。独立したデータベースを使う場合は、本番環境でmessagesテーブルが作成されるよう、rails db:prepareまたはrails db:migrateを実行することを忘れないでください。

また、各WebSocket接続はサーバーのメモリを消費することにも留意しましょう。必要な接続数を捌けるだけのリソースを、サーバーに確保しておくことが大切です。

ポーリング間隔の設定

Solid Cableのポーリング頻度は設定可能で、レイテンシとデータベース負荷のバランスを調整できます。間隔を短くするとポーリングが頻繁になり、新しいメッセージを拾い上げるまでの時間が縮まる反面、データベースへのSELECTクエリが増加します。

逆に、間隔を長くするとデータベースへの負荷は軽くなりますが、ブロードキャストや更新に遅延が生じます。実運用では、デフォルトの0.1秒(毎秒10回のポーリング)が良い出発点です。ほとんどのデータベースを圧迫することなく、体感的にリアルタイムな更新を実現できます。

Solid Cableは「Solidトリフェクタ」を支える重要な柱

この記事では、Rails Action CableがどのようにWebSocketによるリアルタイム通信をもたらすか、そしてSolid CableによってRedisなしでそれが実現できることを見てきました。ところで、Railsには他にもう2つの「Solid」ライブラリがあることをご存じでしょうか? Solid CacheならRedisなしでキャッシュが簡単に行え、Solid QueueならRedisなしでバックグラウンドジョブを処理できます。

「Solidトリフェクタ」を活用すれば、最小限のインフラオーバーヘッドで、インタラクティブなアプリケーションを構築できる非常に強力なフレームワークが手に入ります。

Solid Cableとその仲間たち最大の利点は、そのシンプルさです。Railsアプリのリアルタイム機能は、裏侧にあるのはアプリ自身のデータベースだけという状態で、すぐに動き始めます。デプロイもシンプルになり(Redisや追加サービスが不要)、多くのアプリケーションにとって性能面でも十分すぎるほどです。

もちろん、Railsアプリケーションを本番環境で運用するなら、ユーザーが遭遇しうる問題を監視しておくべきです。Action Cableのコンシューマーやチャンネルに不具合が起きたとき、ユーザーより先に気づけたら素晴らしいと思いませんか?

Honeybadgerは、Railsのエラー監視とパフォーマンス監視に最適なツールであり、リアルタイムアプリケーションの運用には不可欠です。Honeybadgerは、バックエンドとクライアントサイドを含むアプリケーション内のどこでエラーが発生しても即座にアラートを通知し、アプリケーションログやパフォーマンスデータを取り込むことで、迅速な検索・トラブルシューティング・解決を支援します。

Honeybadgerに登録して、今すぐ始めましょう!


  1. Rubyのエイリアス(別名定義)完全ガイド:aliasキーワードとalias_methodの違い

    Rubyでは、既存のメソッドに別名(エイリアス)を付ける方法が2つあります。 alias(キーワード) alias_method(メソッド) どちらも同じ目的で使えますが、挙動が微妙に異なるため、初心者にとって混乱しやすいトピックです。 本記事では、両者の違いを詳しく掘り下げ、しっかりと理解できるように解説していきます。 aliasキーワードとは まずはaliasから見ていきましょう。aliasはRubyのキーワードの一つです(ifやdef、classなどと同じ扱いです)。 基本的な書き方は以下の通りです。 alias print_something puts print_someth

  2. TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

    Rubyでは、メモリの割り当てはどのように行われているのでしょうか? Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。 しかし、 ページがいっぱいになると、さらなるメモリが必要になります。 そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。 このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。 その代表的な実装のひとつが、Google製のTCMallocです。 TCMallocは「gperftools(Goog