Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ActionCable: Repeated subscription attempts #44652

Open
sj26 opened this issue Mar 10, 2022 · 14 comments · May be fixed by #44653
Open

ActionCable: Repeated subscription attempts #44652

sj26 opened this issue Mar 10, 2022 · 14 comments · May be fixed by #44653

Comments

@sj26
Copy link
Contributor

sj26 commented Mar 10, 2022

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:

#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 sj26 linked a pull request Mar 10, 2022 that will close this issue
@sj26
Copy link
Contributor Author

sj26 commented Mar 10, 2022

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.

@salex
Copy link

salex commented Mar 27, 2022

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!

@sj26
Copy link
Contributor Author

sj26 commented Apr 14, 2022

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

@sj26
Copy link
Contributor Author

sj26 commented Apr 14, 2022

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

@rails-bot
Copy link

rails-bot bot commented Jul 13, 2022

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 rails-bot bot added the stale label Jul 13, 2022
@sj26
Copy link
Contributor Author

sj26 commented Jul 13, 2022

This is still reproducible.

@kevinhq
Copy link

kevinhq commented Sep 6, 2022

@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
Copy link

kevinhq commented Sep 13, 2022

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
    }

@lirimkrosa
Copy link

lirimkrosa commented Jul 28, 2023

Check if cable is subscribed to specific channel.

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

@sj26
Copy link
Contributor Author

sj26 commented Sep 10, 2023

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 🙏

@fullc0ntr0l
Copy link

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

@sj26
Copy link
Contributor Author

sj26 commented Sep 10, 2023

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

@x1wins
Copy link

x1wins commented Apr 1, 2024

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)

@nicowenterodt
Copy link
Contributor

nicowenterodt commented Apr 6, 2024

Maybe related: hotwired/turbo-rails#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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants