ActionCableのコネクションのIDについて
この記事は、「フィヨルドブートキャンプ Part 2 Advent Calendar 2024」12日目の記事です。
フィヨルドブートキャンプ Part 2 Advent Calendar 2024 - Adventar
Advent Calender Part1はこちらになります。
フィヨルドブートキャンプ Part 1 Advent Calendar 2024 - Adventar
前回は、jyoumaさんのFBCと歩む私の小さな一歩:感謝と思い切り挑戦する日々でした。
子育てしながら学習をされているjyoumaさん凄いです!
この記事では、Railsの機能であるAction CableのコネクションのIDについて調べたことを書こうと思います。
RailsガイドではコネクションIDの設定方法について記述がありますが、内部でどのようにコネクションIDが決定されるのか気になったため、コードを読んでみました。
コネクションについて
Action Cableのコネクションとは、Action CableサーバーがWebSocket接続を受け取ると作成されるもので、1つのWebSocket接続に対して1つ作成されます。
詳しくはRailsガイドのAction Cableのページを参照してください。
コネクションのIDの設定
Railsガイドには、コネクションの設定の例として以下のようなコードが記載されています。
# app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection end end end end
上記のコードでWebSocket接続が行われた際、以下のような流れでコネクションにIDが設定されます。
identified_by :current_userで、コネクションのIDがcurrent_user属性の値で決定されることを宣言する。- WebSocket接続が確立した際、
connectメソッドが呼ばれる。 ApplicationCable::Connectionインスタンスのcurrent_user属性に、cookieの値から取得したユーザーオブジェクトを代入する。
つまり、ActionCableのコネクションのIDはコネクションインスタンスのcurrent_user属性に保存されているユーザーオブジェクトによって決定される、ということになります。
では、保存された値を元にどのようにしてIDが決定されるのかを追っていきます。
identified_by
はじめにidentified_byメソッドを見ていきます。
このメソッドはActionCable::Connection::Identificationに定義されています。
module ActionCable module Connection module Identification extend ActiveSupport::Concern included do class_attribute :identifiers, default: Set.new end module ClassMethods # Mark a key as being a connection identifier index that can then be used to # find the specific connection again later. Common identifiers are current_user # and current_account, but could be anything, really. # # Note that anything marked as an identifier will automatically create a # delegate by the same name on any channel instances created off the connection. def identified_by(*identifiers) Array(identifiers).each { |identifier| attr_accessor identifier } self.identifiers += identifiers end end
identified_byは以下のことを行なっています。
attr_accessorを呼び出して、受け取った引数のアクセサメソッドを追加する。identifiers属性に引数を追加する。
WebSocket接続時に行なっていた代入(self.current_user = find_verified_user)は、identified_byによってcurrent_userというアクセサメソッドが追加されているため可能となっていました。
identifiersはデフォルトで空の集合となっており、identified_byの引数をその集合に追加しています。
connection_identifier
コネクションのIDはconnection_identifierによって返されます。
このメソッドはidentified_byと同じActionCable::Connection::Identificationに定義されています。
# Return a single connection identifier that combines the value of all the # registered identifiers into a single gid. def connection_identifier unless defined? @connection_identifier @connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") } end @connection_identifier end private def connection_gid(ids) ids.map do |o| if o.respond_to? :to_gid_param o.to_gid_param else o.to_s end end.sort.join(":") end
connection_identifierは以下のことを行なっています。
@connection_identifierが定義されていなければ、@connection_identifierに値を保存する@connection_identifierを返す
@connection_identifierに保存される値を詳しく見てみましょう。
connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") }
identifiers.filter_map { |id| instance_variable_get("@#{id}") }は、identifiersを対応する値に変換しています。
Railsガイドのコードを元に流れを追ってみます。
identifiersはSet[:current_user]Set[:current_user]にfilter_mapが呼ばれる- ブロック内で
instance_variable_get("@current_user")が呼ばれる current_user属性にはself.current_user = find_verified_userで保存したユーザーオブジェクトが格納されているため、そのオブジェクトが返る[user]が返る
そして、取得した配列をconnection_gidに渡しています。
def connection_gid(ids) ids.map do |o| if o.respond_to? :to_gid_param o.to_gid_param else o.to_s end end.sort.join(":") end
引数で受け取った配列の中身を、オブジェクトがto_gid_paramを呼び出せるならばto_gid_paramを呼び出し、呼び出せないならばto_sを呼び出して変換し、変換した値ををソートして連結しています。
to_gid_paramとはなんぞや?と思い調べてみると、次のような実装例がありました。
def to_gid_param to_global_id.to_param end
https://github.com/rails/rails/commit/952887a8bd1fc1e197a482b01c6b1b1ccbcdba2d
モデルのインスタンスに対しto_global_idというメソッドを呼び出しています。
Railsの依存関係の中にglobalidというgemが含まれており、モデルのインスタンスに対してこのメソッドが呼び出せるようになっています。
to_paramメソッドもこのgemで定義されていました。
https://github.com/rails/globalid/blob/e9548a3531a89abfaf036f88d4dac732355a5b80/lib/global_id/global_id.rb#L72
試しに開発中のアプリでメソッドを呼び出してみると、次のような値が返ってきました。
fjord-minutes(dev)> Member.first.to_global_id Member Load (1.0ms) SELECT "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT $1 [["LIMIT", 1]] => #<GlobalID:0x0000000122f8c8e8 @uri=#<URI::GID gid://fjord-minutes/Member/1>> fjord-minutes(dev)> Member.first.to_global_id.to_param Member Load (1.2ms) SELECT "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT $1 [["LIMIT", 1]] => "Z2lkOi8vZmpvcmQtbWludXRlcy9NZW1iZXIvMQ"
ActionCable::Connection::Identificationのコメントにある通り、コネクションのIDの元となる値はユーザーオブジェクト以外でも大丈夫とされています。
以下のコードでは文字列を元にコネクションのIDが決まっています。
まとめ
Action CableコネクションのIDはidentified_byで指定した属性の値を元に決定される。(複数指定も可能)
WebSocket接続時に、identified_byで指定した属性に値を保存しておく必要がある。(保存する値は通常ユーザーオブジェクト)
保存された値はto_gid_paramもしくはto_sで変換され、ソートして:で連結されたものがコネクションのIDとなる。
おわりに
なぜAction Cableのコネクションについて調べたのかというと、開発中の自作サービスでAction Cableを使っているのですが、最初はコネクションIDの設定を追加しておらず、テストを書こうとした際に行き詰まってしまったからです。
Railsのコードを読むことで、Action Cableについての理解を多少なりとも深めることができたと思います。
サーバーのコードもいくつか見てみましたが、その動きを完全に理解するには至りませんでした。これは今後の学習課題としたいと思います。