Resque in Production
Background Everything
@davetron5000
Resque is a Redis-backed Ruby library for creating background jobs, placing them on multiple queues, and processing them later.
class UsersController
  def create
    user = User.new(params[:user])
    if user.save
      UserMailer.welcome(user).deliver
    else
      render 'new'
    end
  end
end
class UsersController
  def create
    user = User.new(params[:user])
    if user.save
      UserMailer.welcome(user).deliver
    else
      render 'new'
    end
  end
end
Set Up Resque
- Write the code
 
- Configure it
 
- Set up worker processes
 
class UsersController
  def create
    user = User.new(params[:user])
    if user.save
      Resque.enqueue(UserWelcomeMailerJob,user.id)
    else
      render 'new'
    end
  end
end
class UserWelcomeMailerJob
  @queue = :mail
  def self.perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver
  end
end
class UserWelcomeMailerJob
  @queue = :mail
  def self.perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver
  end
end
class UserWelcomeMailerJob
  @queue = :mail
  def self.perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver
  end
end
# config/initializers/resque.rb
configuration = {
  host: ENV['RESQUE_REDIS_HOST'],
  port: ENV['RESQUE_REDIS_PORT'],
}
Resque.redis = Redis.new(configuration)
>  echo "require 'resque/tasks'" > lib/tasks/resque.rake
-> Rake task created
>  rake environment resque:work QUEUE=*
-> Worker is now running
New Problem
(the same one, actually)
class PurchasesController
  def create
    customer = current_customer
    purchase = Purchase.new(params[:purchase].merge(customer: customer))
    if purchase.valid?
      Purchase.transaction do
        customer.last_order_date = Time.now
        purchase.save!
        customer.save!
        PurchaseMailer.confirm_purchase(purchase).deliver
      end
    else
      render 'new'
    end
  end
end
class PurchasesController
  def create
    customer = current_customer
    purchase = Purchase.new(params[:purchase].merge(customer: customer))
    if purchase.valid?
      Purchase.transaction do
        customer.last_order_date = Time.now
        purchase.save!
        customer.save!
        PurchaseMailer.confirm_purchase(purchase).deliver
      end
    else
      render 'new'
    end
  end
end
class PurchasesController
  def create
    customer = current_customer
    purchase = Purchase.new(params[:purchase].merge(customer: customer))
    if purchase.valid?
      Purchase.transaction do
        customer.last_order_date = Time.now
        purchase.save!
        customer.save!
        Resque.enqueue(ConfirmPurhaseMailerJob,purchase.id)
      end
    else
      render 'new'
    end
  end
end
class ConfirmPurhaseMailerJob
  @queue = :mail
  def self.perform(purchase_id)
    purchase = Purchase.find(purchase_id)
    PurchaseMailer.confirm_purchase(purchase).deliver
  end
end
class UserWelcomeMailerJob
  @queue = :mail
  def self.perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver
  end
end
class XXXMailerJob
  @queue = :mail
  def self.perform(some_stuff_id)
    some_stuff = SomeStuff.find(id)
    XXXMailer.send_email(some_stuff)
  end
end
>  echo "gem 'resque_mailer'" >> Gemfile
>  bundle install
-> gem installed
class PurchaseMailer < ActionMailer::Base
  def self.confirm_purchase(purchase)
    # ...    
  end
end
class UserMailer < ActionMailer::Base
  
  def self.welcome(user)
    # ...    
  end
end
class PurchaseMailer < ActionMailer::Base
  include Resque::Mailer
  def self.confirm_purchase(purchase_id)
    purchase = Purchase.find(purchase_id)
    # ...    
  end
end
class UserMailer < ActionMailer::Base
  include Resque::Mailer
  def self.welcome(user_id)
    user = User.find(user_id)
    # ...    
  end
end
class PurchaseMailer < ActionMailer::Base
  include Resque::Mailer
  def self.confirm_purchase(purchase_id)
    purchase = Purchase.find(purchase_id)
    # ...    
  end
end
class UserMailer < ActionMailer::Base
  include Resque::Mailer
  def self.welcome(user_id)
    user = User.find(user_id)
    # ...    
  end
end
class UsersController
  def create
    user = User.new(params[:user])
    if user.save
      UserMailer.welcome(user.id).deliver
    else
      render 'new'
    end
  end
end
class PurchasesController
  def create
    customer = current_customer
    purchase = Purchase.new(params[:purchase].merge(customer: customer))
    if purchase.valid?
      Purchase.transaction do
        customer.last_order_date = Time.now
        purchase.save!
        customer.save!
        PurchaseMailer.confirm_purchase(purchase).deliver
      end
    else
      render 'new'
    end
  end
end
Purchase emails still failing
mount Resque::Server, at: 'resque'
class PurchasesController
  def create
    customer = current_customer
    purchase = Purchase.new(params[:purchase].merge(customer: customer))
    if purchase.valid?
      Purchase.transaction do
        customer.last_order_date = Time.now
        purchase.save!
        customer.save!
        PurchaseMailer.confirm_purchase(purchase).deliver
      end
    else
      render 'new'
    end
  end
end
>  echo "gem 'resque-retry'" >> Gemfile
>  bundle install
-> gem installed
# config/initializers/resque.rb
require 'resque_scheduler'
require 'resque-retry'
require 'resque_scheduler/server'
require 'resque-retry/server'
configuration = {
  host: ENV['RESQUE_REDIS_HOST'],
  port: ENV['RESQUE_REDIS_PORT'],
}
Resque.redis = Redis.new(configuration)
require 'resque/failure/redis'
Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
# config/initializers/resque.rb
require 'resque_scheduler'
require 'resque-retry'
require 'resque_scheduler/server'
require 'resque-retry/server'
configuration = {
  host: ENV['RESQUE_REDIS_HOST'],
  port: ENV['RESQUE_REDIS_PORT'],
}
Resque.redis = Redis.new(configuration)
require 'resque/failure/redis'
Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
# config/initializers/resque.rb
require 'resque_scheduler'
require 'resque-retry'
require 'resque_scheduler/server'
require 'resque-retry/server'
configuration = {
  host: ENV['RESQUE_REDIS_HOST'],
  port: ENV['RESQUE_REDIS_PORT'],
}
Resque.redis = Redis.new(configuration)
require 'resque/failure/redis'
Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
# Cribbed from https://blog.engineyard.com/2011/the-resque-way
class AsyncApplicationMailer < ActionMailer::Base
  include Resque::Mailer
  extend Resque::Plugins::Retry
end
class UserMailer < AsyncApplicationMailer
  # ...
end
>  rake environment resque:scheduler
-> retries can now be scheduled and processed
# config/initializers/resque.rb
require 'resque_scheduler'
require 'resque-retry'
require 'resque_scheduler/server'
require 'resque-retry/server'
configuration = {
  host: ENV['RESQUE_REDIS_HOST'],
  port: ENV['RESQUE_REDIS_PORT'],
}
Resque.redis = Redis.new(configuration)
require 'resque/failure/redis'
Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
namespace :monitor do
  namespace :resque do
    task :failures do
      num_failures = Resque::Failure.count
      if num_failures > 0
        Rails.logger.error("#{num_failures} failures in the resque failed queue!")
        ResqueMailer.failed_jobs(num_failures).deliver! # <- bang!
      else
        Rails.logger.info("All clear!")
      end
    end
  end
end
namespace :monitor do
  namespace :resque do
    task :failures do
      num_failures = Resque::Failure.count
      if num_failures > 0
        Rails.logger.error("#{num_failures} failures in the resque failed queue!")
        ResqueMailer.failed_jobs(num_failures).deliver! # <- bang!
      else
        Rails.logger.info("All clear!")
      end
    end
  end
end
class PurchaseChargeJob
  @queue = :purchasing
  def self.perform(purchase_id)
    purchase = Purchase.find(purchase_id)
    purchase.capture_authorization!
    purchase.generate_pack_in_materials!
  end
end
class PurchaseChargeJob
  @queue = :purchasing
  def self.perform(purchase_id)
    purchase = Purchase.find(purchase_id)
    purchase.capture_authorization!
    Resque.enqueue(PackInMaterialsJob,purchase.id)
  end
end
class PackInMaterialsJob 
  @queue = :purchasing
  def self.perform(purchase_id)
    purchase = Purchase.find(purchase_id)
    purchase.generate_pack_in_materials!
  end
end
class PackInMaterialsJob 
  extend Resque::Plugins::Retry
  @queue = :purchasing
  @retry_limit = 5
  @retry_delay = 90 #seconds
  def self.perform(purchase_id)
    purchase = Purchase.find(purchase_id)
    purchase.generate_pack_in_materials!
  end
end
Jobs not being processed, but not failing
class AdminIndexJob
  @queue = :admin
  def self.perform(customer_id)
    # a whole lot of slow code
    # that builds a cached copy of
    # the customer and all his 
    # or her data
  end
end
>  rake environment resque:work QUEUE=mail,purchasing,admin
-> now purchasing takes a back seat
>  rake environment resque:work QUEUE=mail,purchasing,admin
>  rake environment resque:work QUEUE=purchasing,mail,admin
-> now things are getting confusing
>  rake environment resque:work QUEUE=mail
>  rake environment resque:work QUEUE=purchasing
>  rake environment resque:work QUEUE=admin
-> simple, assuming you can spare the CPU and memory
Resque Tips
- Do not use 2.0 - use 1.x
 
- All external contact would be run in a job
 
- All email sent in a job
 
- All jobs configured to retry
 
- All jobs idempotent
 
- Jobs are fine-grained
 
- Each queue has its own worker
 
- Each app has its own Redis
 
- Everything monitored