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: Subscribe uniquely #44653

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

sj26
Copy link
Contributor

@sj26 sj26 commented Mar 10, 2022

If a subscription for the same identifier already exists then it has already been or is being subscribed via the connection, so no need to re-subscribe. Without this change, a subscription message is sent to the server and the guarantor waits for a subscription confirmation which never comes, and so keeps sending subscription messages over and over every second. Fixes #44652.

I'm not sure this is a comprehensive solve, and I'm not sure yet how to test it. I'd love some feedback on the approach. 🙏

I don't think short-circuiting a subscription like this will be a problem. I can't find a place where subscription confirmations are shared back to subscriptions for handling.

@sj26 sj26 changed the title Subscribe uniquely ActionCable: Subscribe uniquely Mar 10, 2022
If a subscription for the same identifier already exists then it has
already been or is being subscribed via the connection, so no need to
re-subscribe. Otherwise the guarantor can get confused and attempt to
subscribe continually.
@sj26 sj26 force-pushed the actioncable-already-subscribed branch from 1a8c8a4 to c559967 Compare March 18, 2022 02:55
If a subscription is created with an identifier already in
subscriptions, and that subscription is not pending in the subscription
guarantor (meaning it is already connected) then fire the connected
callback immediately.
@sj26 sj26 force-pushed the actioncable-already-subscribed branch from dce50ac to a205aca Compare March 21, 2022 01:26
@sj26
Copy link
Contributor Author

sj26 commented Mar 21, 2022

I've added another commit which also immediately fires the connected callback for already-subscribed connections. Otherwise such subscriptions might miss initial state setup, etc.

Check if the subscription exists before adding the new subscription, or
it always will exist.
@sj26
Copy link
Contributor Author

sj26 commented Apr 18, 2022

(The tests on CI seem broken for unrelated reasons?)

Copy link
Contributor

@jorgemanrubia jorgemanrubia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sj26 Thanks for working on this, and great report and troubleshooting in #44652.

I have added a couple of comments, but this change makes a lot of sense 👍. Could you please:

  • Add an entry to the actionable CHANGELOG.
  • Add tests for the changes subscriptions_test.js and subscription_guarantor_test.js. I know those aren't super comprehensive right now, but good to at least test the new behavior.

Thanks! I'll make sure we get this merged.

this.subscribe(subscription)
if (!existing) {
this.subscribe(subscription)
} else if (!this.guarantor.pendingSubscriptions.some((s) => s.identifier === subscription.identifier)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a method to SubscriptionGuarantor to avoid having to access pendingSubscriptions from the outside. E.g: !this.guarantor.hasPendingSubcription(subscription.identifier)

this.subscriptions.push(subscription)
this.consumer.ensureActiveConnection()
this.notify(subscription, "initialized")
this.subscribe(subscription)
if (!existing) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd extract this.findAll(subscription.identifier).length > 0 to a method (e.g: contains(identitfier)) and also remove the separated local variable existing. You could do:

if (!contains(identifier)) {
   ...
}

@palkan
Copy link
Contributor

palkan commented Jun 8, 2022

I don't think short-circuiting a subscription like this will be a problem.

If I understand it correctly, we create two subscription objects, right? The problem may occur if we decide to unsubscribe: the server would unsubscribe the client, while another subscription objet might not know about that and could try to use this stale subscription.

@sj26
Copy link
Contributor Author

sj26 commented Jul 17, 2022

There would be multiple client side subscriptions, and one server subscription. When the last client subscription is unsubscribed, the server subscription will also be unsubscribed and removed. If another client subscription is then created, it will create a new server subscription.

@sj26
Copy link
Contributor Author

sj26 commented Jul 18, 2022

If I were architecting the javascript, I would have an M:N layer multiplexing the client and server subscriptions which also manages the subscription process, instead of the subscription guarantor.

@cam-narzt
Copy link

Anyone know if @rails/actioncable 6.1.4 works with Rails 7? Just wondering if this bug will hold up us upgrading Rails.

@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 this 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 🙏

@dimitrisdovinos
Copy link

This is affecting us too. It would be fantastic if the PR got merged.

Copy link
Contributor

@palkan palkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be fantastic if the PR got merged.

It wouldn't be. This PR is far from solving the problem and may introduce new ones. In addition to some comments I left in the code, there are more things missing:

  • We do not handle the subscriptions.reload() scenario (very important, because reconnections happen often)

  • No tests verifying that this hack works (or not)—that's the biggest problem with this PR.


I think, a proper fix must be implemented within the SubscirpitonGuarantor (which is the root cause of the problem); we already have there a check if the subscription has been monitored, but it compares subscription objects not identifiers:

if(this.pendingSubscriptions.indexOf(subscription) == -1){

Using identifiers instead could probably do the trick. Still. we need a decent test coverage for such race conditions (and that's gonna be the hardest part of the potential PR).

Ideally, we must also take into account unsubscribe requests, especially those happening concurrently with double subscribes. And that would require a proper locking mechanism (not this ad-hoc guarantor thing). This is something we implemented in @anycable/web a while ago—feel free to use it as a drop-in replacement for @rails/actioncable (no need to switch servers for that).

if (!existing) {
this.subscribe(subscription)
} else if (!this.guarantor.pendingSubscriptions.some((s) => s.identifier === subscription.identifier)) {
this.notify(subscription, "connected")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why connected? If it's pending, it could also be rejected as well (or left without any response from the server); we shouldn't mark it as connected here.

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

Successfully merging this pull request may close these issues.

ActionCable: Repeated subscription attempts
5 participants