-
Notifications
You must be signed in to change notification settings - Fork 70
Backburner Tutorial
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.
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!
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.
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
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.
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.
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.