rails icon indicating copy to clipboard operation
rails copied to clipboard

ActionCable: Repeated subscription attempts

Open sj26 opened this issue 2 years ago • 19 comments

Steps to reproduce

  1. Create an actioncable subscription.
  2. Await subscription confirmation.
  3. Create another subscription with the same identifier.
  4. 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:

image

because the guarantor considers the second subscription pending:

image

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.

sj26 avatar Mar 10 '22 04:03 sj26

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.

sj26 avatar Mar 10 '22 22:03 sj26

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,

turbo.err.txt

Beyond my pay grade!

salex avatar Mar 27 '22 18:03 salex

@salex that looks unrelated to the issue I'm describing here.

sj26 avatar Apr 14 '22 05:04 sj26

@matthewd I know you're pretty intimately familiar with action cable, do you have any advice here?

sj26 avatar Apr 14 '22 05:04 sj26

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.

rails-bot[bot] avatar Jul 13 '22 05:07 rails-bot[bot]

This is still reproducible.

sj26 avatar Jul 13 '22 05:07 sj26

@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:

Screenshot from 2022-09-06 15-35-22

kevinhq avatar Sep 06 '22 08:09 kevinhq

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
    }

kevinhq avatar Sep 13 '22 07:09 kevinhq

Check if cable is subscribed to specific channel.

  const isSubscribed = cableConsumer?.subscriptions?.subscriptions
    ?.map((x) => JSON.parse(x.identifier)?.channel)
    ?.includes('CHANNEL')

lirimkrosa avatar Jul 28 '23 09:07 lirimkrosa

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:

sj26 avatar Sep 10 '23 10:09 sj26

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)
}

fullc0ntr0l avatar Sep 10 '23 10:09 fullc0ntr0l

Yeah, that's roughly the patch we carry, as proposed in #44653 🙌

sj26 avatar Sep 10 '23 10:09 sj26

image

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)

x1wins avatar Apr 01 '24 00:04 x1wins

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.

nicowenterodt avatar Apr 06 '24 11:04 nicowenterodt

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
}

kulbirsaini avatar May 30 '24 15:05 kulbirsaini

The hack code I mentioned above is not a fix. I still keep running into this issue.

Screenshot-2024-06-18-123958-pplRBrwT

kulbirsaini avatar Jun 21 '24 04:06 kulbirsaini

I did what many others did, migrated to AnyCable. I have not seen this issue happening since.

kulbirsaini avatar Jul 01 '24 04:07 kulbirsaini

I've recently observed something similar to this, by simply trying to subscribe to a channel that doesn't exist. :thinking:

zzak avatar Jul 01 '24 07:07 zzak