How do I get a list of all ActiveRecord models currently subscribed to via a specific ActionCable Channel?
2 Answers
I'm sorry to give you an answer you do not want, but...:
You don't get a list of all subscribed clients. You shouldn't be able to. If you need this information, you might be experiencing a design flaw.
Why?
The pub/sub paradigm is designed to abstract these details away, allowing for horizontal scaling in a way that has different nodes managing their own subscription lists.
Sure, when you're running a single application on a single process, you might be able to extract this information - but the moment you scale up, using more processes / machines, this information is distributed and isn't available any more.
Example?
For example, when using iodine's pub/sub engine (see the Ruby iodine WebSocket / HTTP server for details):
Each process manages it's own client list.
Each process is a "client" in the master / root process.
Each master / root process is a client in a Redis server (assuming Redis is used).
Let's say you run two iodine "dynos" on Heroku, each with 16 workers, then:
Redis sees a maximum of two clients per channel.
Each of the two master processes sees a maximum of 16 clients per channel.
Each process sees only the clients that are connected to that specific process.
As you can see, the information you are asking for isn't available anywhere. The pub/sub implementation is distributed across different machines. Each process / machine only manages the small part of the pub/sub client list.
EDIT (1) - answering updated question
There are three possible approaches to solve this question:
a client-side solution;
a server-side solution; and
a lazy (invalidation) approach.
As a client side solution, the client could register to a global "server-notifications-channel". When a "re-authenticate" message appears, the client should re-authenticate, initiating the unique token generation on it's unique connection.
A server side solution requires the server-side connection to listen to a global "server-notifications-channel". Then the connection object will re-calculate the authentication token and transmit a unique message to the client.
The lazy-invalidation approach is to simply invalidate all tokens. Connected clients will stay connected (until they close the browser, close their machine or exit their app). Clients will have to re-authenticate when establishing a new connection.
Note (added as discussed in the comments):
The only solution that solves the "thundering herd" scenario is the lazy/invalidation solution.
Any other solution will cause a spike in network traffic and CPU consumption since all connected clients will be processing an event at a similar time.
Implementing:
With ActionCable a client-side solution might be easier to implement. It's design and documentation are very "push" oriented. They often assume a client side processing approach.
On iodine, server-side subscriptions simply require a block
to be passed along to the client.subscribe
method. This creates a client-specific subscription with an event that runs on the server (instead of a message sent to the client).
The lazy-invalidation approach might hurt user experience, depending on the design, since they might have to re-enter credentials.
On the other hand, lazy-invalidation might be the safest, add to the appearance of safety and ease the burden on the servers at the same time.

- 18,516
- 2
- 45
- 67
-
1@FavouriteOnwuemene - I updated my answer. I hope this helps. – Myst Apr 25 '19 at 12:01
-
Considering Redis holds all subscriber information, shouldn't it be possible to get that info from Redis directly? – Favourite Onwuemene Apr 26 '19 at 04:52
-
1@FavouriteOnwuemene - Redis doesn't (or shouldn't) hold all subscriber information. If it does, the pub/sub system is too centralized to properly scale. In my example, Redis holds two subscribers, not all clients... However, you raise an interesting point. Redis might be aware of all **channels** when it's still limited to a single instance (isn't clustered - otherwise, it could be tricky). At this stage of your app, channel names might be used to extract possible subscriber information using the [`PUBSUB CHANNELS` command](https://redis.io/commands/pubsub) (I wouldn't recommend that). – Myst Apr 26 '19 at 05:51
-
1Thanks, @Myst. I earlier experimented with the idea of a Client-side implementation but decided against it because it could cause server load spikes. Imagine there are 1,000 active users/clients, and they all receive a "re-authenticate" message. That is 1,000 request being immediately fired to the server at almost the same time. That is unless the clients can be programmed to re-authenticate in batches. – Favourite Onwuemene Apr 26 '19 at 17:51
-
@FavouriteOnwuemene - You are right, I'll update my answer. The only solution that solves the "thundering herd" scenario is the lazy/invalidation solution. Although WebSocket servers should be able to handle more concurrent traffic than HTTP servers, it's probably more network friendly to spread these re-authentications out. – Myst Apr 27 '19 at 08:54
WARNING: Please see @Myst answer and associated comments. The answer below isn't recommended when scaling beyond a single server instance.
PATCH REQUIRED FOR DEVELOPMENT AND TEST ENVIRONMENT
module ActionCable
module SubscriptionAdapter
class SubscriberMap
def get_subscribers
@subscribers
end
end
end
end
CODE TO GET IDs OF MODELS SUBSCRIBED TO
pubsub = ActionCable.server.pubsub
if Rails.env.production?
channel_with_prefix = pubsub.send(:channel_with_prefix, ApplicationMetaChannel.channel_name)
channels = pubsub.send(:redis_connection).pubsub('channels', "#{channel_with_prefix}:*")
subscriptions = channels.map do |channel|
Base64.decode64(channel.match(/^#{Regexp.escape(channel_with_prefix)}:(.*)$/)[1])
end
else #DEVELOPMENT or Test Environment: Requires patching ActionCable::SubscriptionAdapter::SubscriberMap
subscribers = pubsub.send(:subscriber_map).get_subscribers.keys
subscriptions = []
subscribers.each do |sid|
next unless sid.split(':').size === 2
channel_name, encoded_gid = sid.split(':')
if channel_name === ApplicationMetaChannel.channel_name
subscriptions << Base64.decode64(encoded_gid)
end
end
end
# the GID URI looks like that: gid://<app-name>/<ActiveRecordName>/<id>
gid_uri_pattern = /^gid:\/\/.*\/#{Regexp.escape(SomeModel.name)}\/(\d+)$/
some_model_ids = subscriptions.map do |subscription|
subscription.match(gid_uri_pattern)
# compacting because 'subscriptions' include all subscriptions made from ApplicationMetaChannel,
# not just subscriptions to SomeModel records
end.compact.map { |match| match[1] }

- 4,247
- 8
- 28
- 46