Запуск фоновых заданий в приложении — это отличный способ сохранить пользовательский интерфейс. Будь то отправка электронного письма, вызов какого-либо API или любая длительная задача, всегда есть что-то, что можно перенести в фоновый режим. Иногда имеет смысл настроить очередь и рабочие процессы, но в других случаях было бы здорово просто запустить метод в фоновом режиме с минимальными изменениями кода. Введите Sucker Punch , однопроцессную библиотеку асинхронной обработки. Используя замечательный целлулоидный фреймворк, он позволяет нам выполнять асинхронную работу всего за несколько шагов.
Настроить
Прежде всего, ваше приложение должно работать на Ruby 1.9+, JRuby 1.6+ или Rubinius 2.0 (с режимом Ruby 1.9 на последних 2). Затем установите драгоценный камень, запустив
gem install sucker_punch
или добавьте его в свой Gemfile через
gem 'sucker_punch'
Вот и все! Теперь давайте посмотрим, как его использовать.
Создание работника
Создать работника довольно просто. Это просто обычный класс ruby, который включает в SuckerPunch::Worker
модуль SuckerPunch::Worker
и определяет один или несколько методов экземпляра. Sucker Punch предлагает только один метод execute, но любое имя и количество методов могут быть определены и использованы.
class SomeWorker include SuckerPunch::Worker def perform(some_data) # Method code. Do some work. end end
Вызов работника
Перед вызовом работника нам нужно настроить наши очереди:
SuckerPunch.config do queue name: :some_queue, worker: SomeWorker, workers: 5 queue name: :welcome_queue, worker: WelcomeEmailWorker, workers: 2 end
Мы можем определить столько очередей, сколько мы хотим, с таким количеством рабочих, сколько мы хотим (предпочтительно 2+ на очередь), но у каждого может быть только один рабочий класс на очередь.
Чем больше работников, тем больше параллельных заданий, которые можно выполнить, но имейте в виду, что не хватает соединений, если вы используете соединение с внешней службой, такой как база данных или memcached.
Затем вы можете получить доступ к рабочим в очередях через
SuckerPunch::Queue[:some_queue] # or SuckerPunch::Queue.new(:some_queue)
Это даст нам объект Celluloid::ActorProxy
обертывающий наш рабочий объект.
Мы можем вызвать метод execute (или любой другой метод, который мы определили) непосредственно для этого объекта или использовать async
чтобы мгновенно вернуть управление и заставить его работать в фоновом режиме.
SuckerPunch::Queue[:welcome_queue].perform(1) SuckerPunch::Queue[:welcome_queue].async.perform(1)
Просто имейте в виду, что выполнение асинхронного задания не вызовет никаких исключений в случае сбоя. Это просто потерпит молчание.
тестирование
Конечно, все нужно проверить!
К счастью, тестирование работника довольно просто. Вы можете проверить это как любой класс Ruby.
С некоторым вкусом rspec:
describe WelcomeEmailWorker let(:user){ FactoryGirl.create :user } let(:worker){ EmailWorker.new } describe "#perform" do it "delivers an email" do expect{ worker.perform(user.id) }.to change{ UserMailer.deliveries.size }.by(1) end end end
Чтобы проверить, как он интегрируется с другими методами, есть 2 варианта:
1) Проверьте, что он вызывает и ставит в очередь работу. Для этого вам потребуется 'sucker_punch/testing'
:
require 'sucker_punch/testing' describe User do describe "#send_welcome_email" do it "delivers an email" do let(:user){ FactoryGirl.create :user } expect{ user.send_welcome_email }.to change{ SuckerPunch::Queue.new(:email).jobs.size }.by(1) end end end
2) Выполнение работ на линии. Это может быть сделано с помощью 'sucker_punch/testing/inline'
, и задания всегда будут выполняться синхронно.
require 'sucker_punch/testing' describe User do let(:user){ FactoryGirl.create :user } describe "#send_welcome_email" do it "delivers an email" do expect{ user.send_welcome_email }.to change{ UserMailer.deliveries.size }.by(1) end end end
Соображения
Тесты, выполняющиеся внутри транзакций БД
Есть одна вещь, которую нужно иметь в виду с тестами. Если вы выполняете каждый тест внутри транзакции, вам нужно изменить его на стратегию усечения для тестов Sucker Punch. Вот пример того, как сделать это с DatabaseCleaner: https://gist.github.com/kitop/5248674 . Это происходит потому, что работники Sucker Punch всегда запускают метод в отдельном потоке, независимо от того, является ли он синхронным или асинхронным.
Упорство
Имейте в виду, что Sucker Punch запускает ваши задания в отдельном потоке, а не опрашивает их из внешней очереди. Это означает, что если ваше приложение не работает во время обработки задания, оно не будет ни уведомлять, ни сохранять эту ошибку где-либо по умолчанию, а также не будет повторять ее. Если вам нужно больше контроля над этим, вы можете написать свою собственную оболочку. Возможно, вы могли бы получить вдохновение от girl_friday или использовать другое решение, такое как resque , sidekiq или delayed_job .
Рельсы
Если вы работаете с Rails, рабочие обычно заходят в каталог app/workers
. Вы должны быть осторожны с объектами ActiveRecord и соединениями. Рабочие должны получить идентификатор записи, а не полный объект. Предпочтительно, чтобы работники должны были обернуть код, связанный с доступом к базе данных, в блок ActiveRecord::Base.connection_pool.with_connection
чтобы он не исчерпывал соединения в пуле.
class WelcomeEmailWorker include SuckerPunch::Worker def perform(user_id) ActiveRecord::Base.connection_pool.with_connection do user = User.find(user_id) UserMailer.welcome(user).deliver end end end
связи
Вы должны быть осторожны не только с соединениями ActiveRecord, но также с любым redis, memcache или любым другим сервисом, который может ограничивать соединения. Важно также ограничить количество работников на основе этих ограничений. Вы не хотите иметь 20 рабочих, когда есть максимум 10 соединений!
Unicorn / Пассажирские
Если вы используете Unicorn или Passenger в качестве веб-сервера, есть еще один шаг, чтобы убедиться, что все настроено правильно. То есть определить очереди в блоках, которые запускаются после сервера. Для единорога (необходимо, только если установлено preload_app true
):
# config/unicorn.rb after_fork do |server, worker| SuckerPunch.config do queue name: :log_queue, worker: LogWorker, workers: 10 end end
Для пассажира:
# config/initializers/sucker_punch.rb if defined?(PhusionPassenger) PhusionPassenger.on_event(:starting_worker_process) do |forked| SuckerPunch.config do queue name: :log_queue, worker: LogWorker, workers: 10 end end end