Skip to content

Backburner Tutorial

peter50216 edited this page Nov 25, 2015 · 7 revisions

Introduction

This is a guide towards how to use Backburner in production. In real applications, you will likely have all sorts of jobs, some of which are more important then others and some that require special queues to keep them separated from the other tasks.

By default, every job class in Backburner has a custom queue named after the class. For instance, a NewsletterSender would have a tube called "newsletter-sender". This can all be changed easily, but Backburner provides sensible defaults.

Next, let's imagine a sample application and how this uses Backburner in order to demonstrate practical usage.

SampleBlog

Let's suppose we have an application called SampleBlog which is a very simple blog with certain background tasks. The tasks that need to be backgrounded are as follows:

  • Fetching connected facebook information for a user
  • Sending a new user welcome after signup
  • Sending a reset password email after signup
  • Blog post cache pre-warming when a new post is created

These four tasks are the first things needed to be backgrounded. Let's suppose we already have a UserMailer object setup which sends these emails:

class UserMailer
  def self.deliver_user_welcome(user_id)
    user = User.find(user_id)
    # ...send email to user_id...
  end

  def self.deliver_reset_password(user_id)
    user = User.find(user_id)
    # ...send reset password email to user_id...
  end
end

We also have a User model that sends the appropriate emails using the UserMailer:

class User
  after_create :deliver_welcome_email

  protected

  def reset_password!
    # ...reset password
    UserMailer.deliver_reset_password(self.id)
  end

  def deliver_welcome_email
    UserMailer.deliver_user_welcome(self.id)
  end
end

We also have a method we wrote that fetches a user's facebook account information:

class User
  after_create :fetch_fb_data

  protected

  def fetch_fb_data
    facebook = FacebookFakeClient.new(self.access_token)
    fb_user = facebook.get(:user) # Sends a fb api request
    update_attributes(:avatar => fb_user.profile_picture, :location => fb_user.location)
  end
end

And we currently have no way to pre-warm the cache for a post in our application. Ok, now we want to add Backburner and background process these jobs!

Let's Backburn

Setting up Backburner is pretty straightforward. First, let's install beanstalkd:

$ apt-get install beanstalkd

Next, add to the Gemfile:

# Gemfile

gem 'backburner', '~> 0.0.3'

and then run bundle and you are setup. Next, let's configure our backburner settings:

# app.rb

Backburner.configure do |config|
  config.beanstalk_url = "beanstalk://127.0.0.1"
  config.tube_namespace = "sampleblog.jobs"
  config.on_error = lambda { |e| Airbrake.notify(e) }
end

Here we have setup beanstalk to a local instance and setup a prefix for all backburner tubes. The prefix ensures no tube name collisions with other apps using beanstalkd. We also have every job error reporting to airbrake so we can track jobs that failed and why.

Backgrounding Tasks

Time to start backgrounding tasks. Let's start with the emails that need to be sent for welcome and password reset. If you recall above, we have access to a UserMailer which delivers the mail.

In Backburner, the easiest way to kick off background jobs is to include Backburner::Performable in any object:

class UserMailer
  include Backburner::Performable
  # queue "user-mailer"

  # ... sending emails ...
end

There is no need to change the methods that were already available. Next, when we invoke the methods we can background them by adding async in front of the method call:

class User
  after_create :deliver_welcome_email

  protected

  def reset_password!
    # ...reset password
    UserMailer.async.deliver_reset_password(user_id)
  end

  def deliver_welcome_email
    UserMailer.async.deliver_user_welcome(self.id)
  end
end

And that's all! Now those emails are sent asynchronously through backburner. Next, let's tackle fetching facebook data to store in the user:

class User
  include Backburner::Performable

  after_create lambda { |u| u.async(:queue => "facebook").fetch_fb_data }

  protected
  
  def fetch_fb_data
    # ...same as above...
  end
end

Here we just changed the after_create hook to use async. Notice we also specified the queue as "facebook" so that this job goes into a special job queue and not into the default "user" queue. Now that is fully backgrounded. Finally, let's setup the pre-warming cache. Let's create a new caching class:

class PostPrecache
  include Backburner::Performable
  
  # Caches the post into memcache after creation so that it is fast for all readers.
  def self.cache(post_id)
    post = Post.find(post_id)
    Padrino.cache.fetch("post-#{post_id}-html") { post.to_html }
  end
end

and then use this when a post is created:

class Post
  after_create :prewarm_cache

  def prewarm_cache
    PostPrecache.async.cache(self.id)
  end
end

Processing Jobs

Ok, now all our jobs are backgrounded successfully. Now time to setup workers to process these jobs. For now, let's setup one worker that processes everything.

Development

The best way to start a worker in development mode is to use the rake task directly in the foreground:

cd /path/to/rails/app
rake backburner:work

will process all jobs for your application on all known queues. You can also process the jobs on just one or more queues as well:

QUEUES=newsletter-sender,push-message rake backburner:work

and this will process jobs from these particular queues right in the terminal. This makes debugging jobs and watching processing easy while you are developing.

Production

The best way to do this in production is using God. God will start the worker and make sure the worker logs correctly and restarts if it crashes. Let's create a god recipe for the first worker:

# /etc/god/sample-blog-worker-1
God.watch do |w|
  w.name   = "sample-blog-worker-1"
  w.dir    = '/path/to/app/dir'
  w.env = { 'PADRINO_ENV' => 'production', 'QUEUES' => 'user-mailer,facebook,post-precache' }
  w.group    = 'sample-blog-workers'
  w.interval = 30.seconds
  w.start = "bundle exec rake -f Rakefile backburner:start"
  w.log   = "/var/log/god/sample-blog-worker-1.log"

  # restart if memory gets too high
  w.transition(:up, :restart) do |on|
    on.condition(:memory_usage) do |c|
      c.above = 50.megabytes
      c.times = 3
    end
  end

  # determine the state on startup
  w.transition(:init, { true => :up, false => :start }) do |on|
    on.condition(:process_running) do |c|
      c.running = true
    end
  end

  # determine when process has finished starting
  w.transition([:start, :restart], :up) do |on|
    on.condition(:process_running) do |c|
      c.running = true
      c.interval = 5.seconds
    end

    # failsafe
    on.condition(:tries) do |c|
      c.times = 5
      c.transition = :start
      c.interval = 5.seconds
    end
  end

  # start if process is not running
  w.transition(:up, :start) do |on|
    on.condition(:process_running) do |c|
      c.running = false
    end
  end
end

Notice the QUEUES which specifies the jobs to work. Now you can start the worker with god:

$ god start sample-blog-workers

and you should be good to go. The worker will process jobs and your application will properly enqueue them.