rails
rails copied to clipboard
ActionCable: Repeated subscription attempts
Steps to reproduce
- Create an actioncable subscription.
- Await subscription confirmation.
- Create another subscription with the same identifier.
- Observe websocket showing subscription attempts every second.
I tried creating a small reproduction script, but involving views and action cable got too complicated. Instead, here's an app which replicates it:
https://github.com/sj26/action_cable_test
It creates two subscriptions with the same identifier, with a timeout to be sure that the first subscription has already been confirmed:
<div id="count"></div>
<%= javascript_tag do %>
window.App.consumer.subscriptions.create({channel: "TestChannel"}, {
received({ count }) {
document.getElementById("count").innerText = count;
}
});
<% end %>
<div id="contents"></div>
<%= javascript_tag do %>
setTimeout(function() {
window.App.consumer.subscriptions.create({channel: "TestChannel"}, {
received({ contents }) {
document.getElementById("contents").innerText = contents;
}
});
}, 1000)
<% end %>
Then a stream of subscription messages can be seen in the websocket traffic:

because the guarantor considers the second subscription pending:

because actioncable short circuits existing subscriptions, and so doesn't submit multiple confirmations:
https://github.com/rails/rails/blob/v7.0.2/actioncable/lib/action_cable/connection/subscriptions.rb#L32
Use case
We have a react frontend, and we want to subscribe to channels in each of the leaf components which require dynamic updates directly. Sometimes this means multiple components will be interested in the same updates, and subscribe to the same identifiers. Trying to deduplicate these subscriptions somehow requires a lot more state management which seems more fragile and uneccessary. Previous versions of actioncable before the guarantor seem to work great for this use case, and the actioncable implemenation seems to consider subscription duplication in all operations. It's the addition of the subscription guarantor in #41581 to fix #38668 which seems to have created this issue. I think it would continue to work great with some gentle adjustment.
Expected behavior
Subscribing to the same channel identifier multiple times should not constantly send subscription messages.
Actual behavior
A continuous stream of subscription messages are sent from the browser and arrive and are squashed at the server without any confirmation message returning. This taxes both the browser client and the action cable server unnecessarily.
System configuration
Rails version: 7.0.2
Ruby version: 3.1.0
Potential solution
The simplest solution seems to be to follow the pattern established in the rest of Subscriptions and avoid re-subscribing subscriptions which already exist:
https://github.com/rails/rails/pull/44653
It's also a little confusing. The "subscription" in the javascript environment does not necessarily have a 1:1 relationship with the "subscription" on the server. This is a good thing! But it'd be nice if they had different names.
Oh I guess the other solution would be to send a subscription confirmation from the actioncable server when the client asks for a subscription which is already in place, instead of swallowing it silently. That doesn’t seem like a terrible idea. Although I’m not sure if swallowing it is intentional, and solves any other issues.
I think I'm having the same or similar problem.
I tried to implement DHH's Rails 7 Blog/post demo in an application with some modifications. Mainly added a user and group that owned the post, converted the Slim, etc. I got everything working except the broadcast. I finally found the Demo Code (kinda hard to use the screencast to build code with all the jumping around) and found I had a DOM id problem. I worked in development. But then deployed it to a staging server and BOOM.
If I tried to go to Discussions(post) I got a Rails 502 error. Clicking refresh would bring up the post. Trying to open another browser window would get the rails error again on the first click, the open it.
I would not stream and I also had some javascript (stimulus) that was supposed to highlight some markup but it just had a blank field. In other words, nothing worked.
I took out the streaming and redeployed and all other areas worked fine, just no streaming.
Here are about 20 lines each of rails, nginx and puma error logs,
Beyond my pay grade!
@salex that looks unrelated to the issue I'm describing here.
@matthewd I know you're pretty intimately familiar with action cable, do you have any advice here?
This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 7-0-stable
branch or on main
, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.
This is still reproducible.
@sj26 any update on this? i have same problem to solve. multiple chat rooms, when I moved to another room, then go back to the first one or previous room, it creates another subscription with duplicated identifier:
Update for whoever had same problem with same scenario with multiple chat rooms: I ended up using javascript to prevent creating/subscribing to server when the existing subscription already existed:
var arr_of_identifiers = consumer.subscriptions.subscriptions.map(s => {
return s.identifier
});
var is_subscribed = false;
for (const identifier of arr_of_identifiers) {
if(identifier.includes(chatRoomId)) {
is_subscribed = true;
break;
}
}
console.log(is_subscribed);
if(is_subscribed == false) {
// subscribe to server
}
Check if cable is subscribed to specific channel.
const isSubscribed = cableConsumer?.subscriptions?.subscriptions
?.map((x) => JSON.parse(x.identifier)?.channel)
?.includes('CHANNEL')
Sorry I don't have any updates. I do think this is still an issue. We carry a patch for it in our codebase. But I don't have capacity to push it to completion myself. If someone else is keen, please let me know :pray:
Using this workaround for now
const subscription = consumer.subscriptions.create(...createArgs)
const subscriptionExists = consumer.subscriptions["findAll"](subscription.identifier).length > 1
if (subscriptionExists) {
consumer.subscriptions["confirmSubscription"](subscription.identifier)
}
Yeah, that's roughly the patch we carry, as proposed in #44653 🙌
It's still happen. Why ActionCable server don't send 'subscription confirm'?
Gemfile
ruby "3.2.2"
gem "rails", "~> 7.0.8"
Gemfile.lock
rails (7.0.8)
actioncable (= 7.0.8)
Maybe related: https://github.com/hotwired/turbo-rails/issues/173
TL;TR:
I see something similar with actioncable (7.1.3.2).
When I have a turbo_stream_from
tag in place to connect my client to a stream and the client then navigates forth and back (using turbo-drive) to other pages where this tag is also in present, I see multiple unsubscribes
and subscribes
in my websocket monitor which occasionally leads to an unsubscribe from a stream which should be still connected.
When "hard-reloading" the page the websocket subscription to that stream is established as expected. So this only happens when a client navigates through the page.
This leads to the websocket not receiving messages anymore because the stream has been unsubscribed which leads to:
Turbo::StreamsChannel stopped streaming from ....
Occasionally throwing:
Could not execute command from ({"command"=>"unsubscribe" ....})
[RuntimeError - Unable to find subscription with identifier: ... ]
Imho there should be no unsubscribe at all when turbo-drive checks there is the same stream present again on the page the client navigates to.
I'm still trying to understand, however this seems related. Feels a bit like a race-condition.
This caused much pain for me today. Thank you for suggested hacks. I am using this for now.
function createSubscription () {
window.actionCableConsumer = window.actionCableConsumer || createConsumer()
const subscription = window.actionCableConsumer.subscriptions.create(channelOptions)
// Check if we are already subscribed to this channel. This is to avoid browser banging the server with subscribe requests
// https://github.com/rails/rails/issues/44652#issuecomment-1712780124
if (window.actionCableConsumer.subscriptions["findAll"](subscription.identifier).length > 1) {
window.actionCableConsumer.subscriptions["confirmSubscription"](subscription.identifier)
}
return subscription
}
The hack code I mentioned above is not a fix. I still keep running into this issue.
I did what many others did, migrated to AnyCable. I have not seen this issue happening since.
I've recently observed something similar to this, by simply trying to subscribe to a channel that doesn't exist. :thinking: