What can be the reason of “Unable to find subscrip

2020-07-16 02:17发布

问题:

I'm building a messenger application using Rails 5.0.0.rc1 + ActionCable + Redis.

I've single channel ApiChannel and a number of actions in it. There are some "unicast" actions -> ask for something, get something back, and "broadcast" actions -> do something, broadcast the payload to some connected clients.

From time to time I'm getting RuntimeError exception from here: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/subscriptions.rb#L70 Unable to find subscription with identifier (...).

What can be a reason of this? In what situation can I get such exception? I spent quite a lot of time on investigating the issue (and will continue to do so) and any hints would be greatly appreciated!

回答1:

It looks like it's related to this issue: https://github.com/rails/rails/issues/25381

Some kind of race conditions when Rails reply the subscription has been created but in fact it hasn't been done yet.

As a temporary solution adding a small timeout after establishing the subscription has solved the issue.

More investigation needs to be done, though.



回答2:

The reason for this error might be the difference of the identifiers you subscribe to and messaging to. I use ActionCable in Rails 5 API mode (with gem 'devise_token_auth') and I faced the same error too:

SUBSCRIBE (ERROR):

{"command":"subscribe","identifier":"{\"channel\":\"UnreadChannel\"}"}

SEND MESSAGE (ERROR):

{"command":"message","identifier":"{\"channel\":\"UnreadChannel\",\"correspondent\":\"client2@example.com\"}","data":"{\"action\":\"process_unread_on_server\"}"}

For some reason ActionCable requires your client instance to apply the same identifier twice - while subscribing and while messaging:

/var/lib/gems/2.3.0/gems/actioncable-5.0.1/lib/action_cable/connection/subscriptions.rb:74

def find(data)
  if subscription = subscriptions[data['identifier']]
    subscription
  else
    raise "Unable to find subscription with identifier: #{data['identifier']}"
  end
end

This is a live example: I implement a messaging subsystem where users get the unread messages notifications in the real-time mode. At the time of the subscription, I don't really need a correspondent, but at the messaging time - I do.

So the solution is to move the correspondent from identifier hash to the data hash:

SEND MESSAGE (CORRECT):

{"command":"message","identifier":"{\"channel\":\"UnreadChannel\"}","data":"{\"correspondent\":\"client2@example.com\",\"action\":\"process_unread_on_server\"}"}

This way the error is gone.

Here's my UnreadChannel code:

class UnreadChannel < ApplicationCable::Channel
  def subscribed

    if current_user

      unread_chanel_token = signed_token current_user.email

      stream_from "unread_#{unread_chanel_token}_channel"

    else
# http://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#class-ActionCable::Channel::Base-label-Rejecting+subscription+requests
      reject

    end

  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def process_unread_on_server param_message

    correspondent = param_message["correspondent"]

    correspondent_user = User.find_by email: correspondent

    if correspondent_user

      unread_chanel_token = signed_token correspondent

      ActionCable.server.broadcast "unread_#{unread_chanel_token}_channel",
                                   sender_id: current_user.id
    end

  end

end

helper: (you shouldn't expose plain identifiers - encode them the same way Rails encodes plain cookies to signed ones)

  def signed_token string1

    token = string1

# http://vesavanska.com/2013/signing-and-encrypting-data-with-tools-built-in-to-rails

    secret_key_base = Rails.application.secrets.secret_key_base

    verifier = ActiveSupport::MessageVerifier.new secret_key_base

    signed_token1 = verifier.generate token

    pos = signed_token1.index('--') + 2

    signed_token1.slice pos..-1

  end  

To summarize it all you must first call SUBSCRIBE command if you want later call MESSAGE command. Both commands must have the same identifier hash (here "channel"). What is interesting here, the subscribed hook is not required (!) - even without it you can still send messages (after SUBSCRIBE) (but nobody would receive them - without the subscribed hook).

Another interesting point here is that inside the subscribed hook I use this code:

stream_from "unread_#{unread_chanel_token}_channel"

and obviously the unread_chanel_token could be whatever - it applies only to the "receiving" direction.

So the subscription identifier (like \"channel\":\"UnreadChannel\") has to be considered as a "password" for the future message-sending operations (e.g. it applies only to the "sending" direction) - if you want to send a message, (first send subscribe, and then) provide the same "pass" again, or you'll get the described error.

And more of it - it's really just a "password" - as you can see, you can actually send a message to whereever you want:

ActionCable.server.broadcast "unread_#{unread_chanel_token}_channel", sender_id: current_user.id

Weird, right?

This all is pretty complicated. Why is it not described in the official documentation?