В чем может быть причина невозможности найти подписку с идентификатором в Rails ActionCable?

Я создаю приложение для обмена сообщениями, используя Rails 5.0.0.rc1 + ActionCable + Redis.

У меня есть один канал ApiChannel и ряд действий в нем. Есть некоторые «одноадресные» действия -> запросить что-то, получить что-то в ответ и «широковещательные» действия -> что-то сделать, передать полезную нагрузку некоторым подключенным клиентам.

Время от времени я получаю исключение RuntimeError отсюда: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/subscriptions.rb#L70 Unable to find subscription with identifier (...).

Что может быть причиной этого? В какой ситуации я могу получить такое исключение? Я потратил довольно много времени на изучение проблемы (и буду продолжать это делать), и любые подсказки будут очень признательны!


person Tomek Wałkuski    schedule 08.06.2016    source источник
comment
К вашему сведению, это было исправлено с помощью github.com/rails/rails/pull/26547.   -  person Tomek Wałkuski    schedule 25.04.2017


Ответы (2)


Похоже, это связано с этой проблемой: https://github.com/rails/rails/issues/25381

Какие-то условия гонки, когда Rails отвечает, что подписка создана, но на самом деле это еще не сделано.

Как временное решение, добавление небольшого тайм-аута после установления подписки решило проблему.

Однако необходимо провести дополнительное расследование.

person Tomek Wałkuski    schedule 06.07.2016

Причиной этой ошибки может быть различие идентификаторов, на которые вы подписываетесь, и на которые вы обмениваетесь сообщениями. Я использую ActionCable в режиме API Rails 5 (с gem 'devise_token_auth'), и я тоже столкнулся с той же ошибкой:

ПОДПИСАТЬСЯ (ОШИБКА):

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

ОТПРАВИТЬ СООБЩЕНИЕ (ОШИБКА):

{"command":"message","identifier":"{\"channel\":\"UnreadChannel\",\"correspondent\":\"[email protected]\"}","data":"{\"action\":\"process_unread_on_server\"}"}

По какой-то причине ActionCable требует, чтобы ваш экземпляр клиента дважды применял один и тот же идентификатор — при подписке и при обмене сообщениями:

/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

Это живой пример: я реализую подсистему обмена сообщениями, где пользователи получают уведомления о непрочитанных сообщениях в режиме реального времени. На момент подписки мне correspondent особо не нужен, а на момент обмена сообщениями - нужен.

Таким образом, решение состоит в том, чтобы переместить correspondent из хэша идентификатора в хэш данных:

ОТПРАВИТЬ СООБЩЕНИЕ (ПРАВИЛЬНО):

{"command":"message","identifier":"{\"channel\":\"UnreadChannel\"}","data":"{\"correspondent\":\"[email protected]\",\"action\":\"process_unread_on_server\"}"}

Таким образом, ошибка исчезла.

Вот мой код UnreadChannel:

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: (вы не должны выставлять простые идентификаторы — кодируйте их так же, как Rails кодирует обычные файлы cookie в подписанные)

  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  

Подводя итог всему этому, вы должны сначала вызвать команду SUBSCRIBE, если вы хотите позже вызвать команду MESSAGE. Обе команды должны иметь одинаковый хеш-идентификатор (здесь «канал»). Что интересно, хук subscribed не требуется (!) - даже без него можно отправлять сообщения (после SUBSCRIBE) (но их никто не получит - без хука subscribed).

Еще один интересный момент заключается в том, что внутри хука subscribed я использую этот код:

stream_from "unread_#{unread_chanel_token}_channel"

и, очевидно, unread_chanel_token может быть любым - это относится только к "принимающему" направлению.

Таким образом, идентификатор подписки (например, \"channel\":\"UnreadChannel\") следует рассматривать как "пароль" для будущих операций отправки сообщений (например, он применяется только к направлению "отправка"). - если вы хотите отправить сообщение, (сначала отправить подписку, а затем) предоставить тот же "пароль" еще раз, иначе вы получите описанную ошибку.

И более того — на самом деле это просто «пароль» — как видите, вы действительно можете отправить сообщение куда угодно:

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

Странно, да?

Это все довольно сложно. Почему это не описано в официальной документации?

person prograils    schedule 18.04.2017
comment
Это мой вопрос точно. Спасибо, что так ясно изложили. Я столкнулся с той же проблемой. - person Melvin Roest; 11.10.2019