Обратные вызовы ActiveSupport

В моем последнем посте я коснулся pry и того, как это помогло мне убедиться, что к моему классу прикреплены обратные вызовы around_performActiveSupport. В этом посте я подробнее расскажу о том, что я пытался сделать.

[ActiveJob, Sidekiq] — [ActiveJob]

Я работал над проектом Rails 4.2.x, в котором выполнялась фоновая обработка заданий. Мы использовали ActiveJob в качестве нашего адаптера для наших фоновых заданий. Негласно мы использовали драгоценный камень Sidekiq.

В конце концов нам понадобились подробности, которые может предоставить только родной Sidekiq через его sidekiq_options. Эти опции, которые предоставляет Sidekiq, изначально нам не нужны. Как упоминается в Sidekiq Wiki:

Обратите внимание, что более продвинутые функции Sidekiq (sidekiq_options) нельзя контролировать или настраивать через ActiveJob, например. сохранение следов.

Пришло время воспользоваться преимуществами мощных драгоценных камней и опций sidekiq, поэтому нам нужно переключиться с ActiveJob на собственный Sidekiq.

Бесплатные привилегии ActiveJob

ActiveJob автоматически предоставлял определенные функции, такие как использование GlobalID и настройка обратных вызовов, среди прочего. С нативным подходом Sidekiq мы теряем эти бесплатные привилегии.

Больше всего мы пропустили обратные вызовы, особенно around_perform. У нас было несколько модулей, которые были смешаны с нашими классами заданий, с единственной обязанностью дополнить класс обратными вызовами.

Например:

module JobMetrics
  extend ActiveSupport::Concern
  included do
    around_perform do |_job, block|
      MetricsLogger.timing(metrics_logger_key) { block.call }
    end
    def metrics_logger_key
      @metrics_logger_key ||= signature.underscore.tr("/", ".")
    end
  end
end

Этот модуль упаковывает #perform фактического задания в файл MetricsLogger.timing. В будущем посте я могу подробно рассказать о MetricsLogger, но по своей сути он записывает ключ/значение и отправляет его в агрегатор журналов. Преимущество, которое мы получаем от этого модуля, заключается в возможности узнать временные показатели для заданий на основе идентифицирующей подписи.

Отходя от ActiveJob, нам нужен другой способ сделать то же самое (содержащие обратные вызовы) только с помощью Sidekiq.

Содержащиеся обратные вызовы

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

Добавить прокси

Я обнаружил, что для использования ActiveSupport::Callbacks нужно изменить исполняемый метод, которым в нашем случае будет #perform задания.

...
  def perform
    run_callbacks :perform do
      # Actual perform's content here
    end
  end
...

Я не хотел изменять определения методов #perform для всех заданий. Поэтому я придумал решение использовать prepend для размещения прокси-сервера перед рабочими местами #perform.

module SidekiqCallbacks
  extend ActiveSupport::Concern
  def perform(*args)
    run_callbacks :perform do
      super(*args)
    end
  end
end

Затем этот модуль может быть добавлен в классы заданий Sidekiq, и будут выполняться обратные вызовы — если они присутствуют. Следующая задача — поддержка обратного вызова around_perform.

Поддержка настройки и запуска обратных вызовов

require "active_support/callbacks"
# Following approach used by ActiveJob
# https://github.com/rails/rails/blob/93c9534c9871d4adad4bc33b5edc355672b59c61/activejob/lib/active_job/callbacks.rb
module SidekiqCallbacks
  extend ActiveSupport::Concern
  def perform(*args)
    if respond_to?(:run_callbacks)
      run_callbacks :perform do
        super(*args)
      end
    else
      super(*args)
    end
  end
  module ClassMethods
    def around_perform(*filters, &blk)
      set_callback(:perform, :around, *filters, &blk)
    end
  end
end

Теперь SidekiqCallbacks определяет возможность добавления обратных вызовов, и они будут выполняться до #perform, если они определены.

Завершение

Последнее, что я хочу сделать, — это инкапсулировать логику обратного вызова Sidekiq в отдельный модуль, определяющий фактический обратный вызов (т. е. JobMetrics). Для этого нам нужно дополнительно изменить SidekiqCallbacks.

require "active_support/callbacks"
# Following approach used by ActiveJob
# https://github.com/rails/rails/blob/93c9534c9871d4adad4bc33b5edc355672b59c61/activejob/lib/active_job/callbacks.rb
module SidekiqCallbacks
  extend ActiveSupport::Concern
  def self.prepended(base)
    base.include(ActiveSupport::Callbacks)
    # Check to see if we already have any callbacks for :perform
    # Prevents overwriting callbacks if we already included this module (and defined callbacks)
    base.define_callbacks :perform unless base.respond_to?(:_perform_callbacks) && base._perform_callbacks.present?
    class << base
      prepend ClassMethods
    end
  end
  def perform(*args)
    if respond_to?(:run_callbacks)
      run_callbacks :perform do
        super(*args)
      end
    else
      super(*args)
    end
  end
  module ClassMethods
    def after_perform(*filters, &blk)
      set_callback(:perform, :after, *filters, &blk)
    end
  end
end

Нам пришлось включить self.prepended, чтобы класс задания имел доступ к определенным методам через содержащийся модуль обратного вызова. Здесь важно отметить, что мы включаем ActiveSupport::Callbacks в базовый объект, который предшествует этому модулю. Мы также должны убедиться, что обратные вызовы определены только один раз (именно здесь в моем последнем посте я использовал pry рисунок, почему не все мои обратные вызовы были определены).

module JobMetrics
  extend ActiveSupport::Concern
  included do
    prepend SidekiqCallbacks
    around_perform do |_job, block|
      MetricsLogger.timing(metrics_logger_key) { block.call }
    end
    def metrics_logger_key
      @metrics_logger_key ||= signature.underscore.tr("/", ".")
    end
  end
end

Наконец, мы можем видеть, как JobMetrics имеет новый prepend SidekiqCallbacks, и он включает всю необходимую логику ActiveSupport::Callback, которая позволяет определять и выполнять обратные вызовы.

Победа

Преимущество такого подхода состоит в том, что реализация обратного вызова полностью содержится в модуле JobMetrics. Модуль SidekiqCallbacks обеспечивает отсутствующую поддержку обратного вызова ActiveJob для around_perform. С помощью этого подхода также можно добавить отсутствующие обратные вызовы ActiveJob.

В конце конкретные классы заданий содержат только include содержащийся модуль обратного вызова (т. е. JobMetrics). SidekiqCallbacks предназначен для размещения нескольких содержащихся модулей обратного вызова, включенных в один конкретный класс заданий.

Первоначально опубликовано на kevinjalbert.com.