Обратные вызовы Active Record перестали работать последовательно после обновления до Rails 5 - PullRequest
0 голосов
/ 21 ноября 2019

Я недавно обновил приложение с Rails 4.2 до 5.2. Это приложение включает несколько обратных вызовов активных записей, где пользователь вводит значение или нажимает кнопку, и для продолжения работы с программой выдается обратный вызов. После обновления эти обратные вызовы больше не работают последовательно в производстве. Локально все работает хорошо, но после развертывания (с использованием capistrano) регулярно требуется 3-6 попыток получить обратный вызов. Вот мой класс обратного вызова:

module CallAfterCommit
  def self.included(base)
    base.after_commit :notify_commit # piggyback off active_record hook.
    base.include(InstanceMethods)
    base.extend(ClassMethods)
  end

  ##
  # Instance methods for CallAfterCommit mixin.
  module InstanceMethods
    ##
    # Takes a callback that will be called whenever any instance of this model with the same id is committed. The instance
    # this method is called on must have an id set.
    def add_after_commit_callback(&block)
      raise "#{self} does not have an id yet, so it could not add callback" if id.nil?

      self.class.add_after_commit_callback(id, &block)
    end

    ##
    # Removes instances of callback to be called when a model with this id is committed.
    def remove_after_commit_callback(callback)
      self.class.remove_after_commit_callback(id, callback)
    end

    ##
    # Returns true if the callback is registered for the model's id, and returns false otherwise.
    def after_commit_callback?(callback)
      self.class.after_commit_callback?(id, callback)
    end

    private

    def notify_commit
      self.class._notify_commit(id)
    end
  end

  ##
  # Class methods for CallAfterCommit mixin.
  module ClassMethods
    ##
    # Add a callback to a set of callbacks for the given id. to be called after any instance with that id is committed.
    # Returns the callback as a proc, which can be used to remove the after commit callback
    def add_after_commit_callback(id, &block)
      raise ArgumentError, "Cannot add callback. No record of id #{id} exists for #{self}." unless exists?(id)
      raise ArgumentError, "Cannot add callback. Callback already exists for #{id}." if after_commit_callback?(id, block)

      registered_callbacks[id] ||= []
      registered_callbacks[id] |= [block]

      block
    end

    ##
    #  Removes the callback for a given id. Raises an error if the callback cannot be removed.
    def remove_after_commit_callback(id, callback)
      raise "Could not remove callback because it is not registered for id #{id}" unless after_commit_callback?(id, callback)

      registered_callbacks[id].delete(callback)
      # Remove the hash entry to the empty array, so we don't collect every id ever listened to.
      registered_callbacks.delete(id) if registered_callbacks[id].empty?
    end

    ##
    # Returns true if the callback is registered for the given id, and returns false otherwise.
    def after_commit_callback?(id, callback)
      !registered_callbacks[id].nil? && registered_callbacks[id].include?(callback)
    end

    ##
    #  Execute all the callbacks registered for this id.
    def _notify_commit(id)
      registered_callbacks[id]&.each(&:call)
    end

    private

    ##
    # Returns a hash of ids to an array of callbacks
    def registered_callbacks
      # hash of ids to an array of callbacks
      @registered_callbacks ||= {}
    end
  end
end

А вот одно из мест, где используется упомянутый обратный вызов:

module WaitForUserSimple
  def self.included(base)
    base.after_initialize :setup_new_instance, if: :new_record? # piggyback off active_record hook.
  end

  ##
  # Receive message from the user. Expects params to have a key for :user_choice and looks for 'ok' or 'quit'
  def receive_user_data(params)
    logger.info 'received user data'
    details.user_choice = params[:user_choice]
    details.save!
  end

  def stop
    logger.info 'Stopping simple user step'
    receive_user_data(user_choice: 'quit')
  end

  private

  def setup_new_instance
    # We need this detail when we initialize, because we watch it when waiting for the user.
    self.details ||= SimpleUserStepDetail.new(test_step: self)
  end

  def run_test_step
    event = Concurrent::Event.new # Use this to let us know if there has been a save since we reloaded the details.

    callback = details.add_after_commit_callback do
      logger.info("Callback called. Setting event for #{self}")
      event.set
    end

    while details.reload.user_choice.nil?
      logger.info("#{self} waiting for details to be saved")
      event.wait
      event.reset
      logger.info("#{self} notified that event was saved. Checking details.")
    end

    details.remove_after_commit_callback(callback)
    logger.info("Got user choice: #{details.user_choice}")
    details.user_choice == 'ok'
  end
end

Нет сообщений об ошибках или операторов регистрации, чтобы показать, что происходит,У кого-нибудь есть предложения, что может быть не так?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...