9.5. Dealing with Long-Running TasksWhen an action takes a long time to executesay, minutes or even hoursthe usual request/response cycle for web interfaces breaks down. If a request is slow to finish, most users will assume that something isn't working and try the request again, which in many cases is the worst thing they can do because it will double the workload on the server and still not return feedback to the user. The ideal solution is for the action to create a background thread that carries out the work, while responding to the original web request immediately. Then, the browser can use Ajax to get periodic status updates from the server on the progress of the job. Sound complicated? Thanks to the BackgrounDRb plug-in, it's surprisingly simple. BackgrounDRb (http://backgroundrb.rubyforge.org) makes the process of working with background jobs in Rails fairly painless. The plug-in creates a separate instance of your Rails application running on a DRb server, and provides a MiddleMan object for your Rails application to interact with it. For example, suppose you are creating a system to manage email newsletter campaigns. Sending thousands of emails at once will take a while, so BackgrounDRb can make the process smoother. Here's how the Campaign model might look. class Campaign < ActiveRecord::Base belongs_to :message has_many :recipients def start MiddleMan.new_worker :class => :campaign_worker, :args => id, :job_key => id end def worker; MiddleMan[id]; end delegate :total, :progress, :to => :worker end This example illustrates an ActiveRecord model named Campaign, which has two associations (message and recipients) and a start method. The last two lines delegate two methods to the BackgrounDRb worker that will be created for each Campaign instance. When Campaign#start is called, a new BackgrounDRb worker is instantiated to handle delivering the emails. The worker is defined in lib/workers/campaign_worker.rb: class CampaignWorker < BackgrounDRb::Rails # Create attributes that can be polled to get the job status attr_reader :progress attr_reader :total def do_work campaign_id campaign = Campaign.find campaign_id recipients = campaign.recipients @total = recipients.size @progress = if recipients.any? 0 else 100 # if there are no recipients, we are done! end recipients.each_with_index do |recipient, i| @progress = (((i+1).to_f/@total)*100).round Notifier.deliver_message :email => recipient.email, :name => recipient.name, :message => campaign.message end end end BackgrounDRb automatically invokes the do_work method in the background server. Between Campaign and CampaignWorker, you've got some idea of what the backend looks like. But what about the controller and views? Here's what the controller code could look like. We'll define two actions, create and show, and use inline RJS in both of them: class CampaignsController < ApplicationController # Create the new campaign and instruct the page to # request the campaign's #show action with Ajax. def create campaign = Campaign.create params[:campaign] campaign.start render :update do |page| page << remote_function(:url => campaign_url(:id => campaign), :method => :get) end end # Update the page's progress bar, then either re-request this # action or alert the user that the job is done. def show @campaign = Campaign.find params[:id] render :update do |page| page[:progressbar].setStyle :width => "#{@campaign.progress * 2}px" page[:progressbar].replace_html "#{@campaign.progress}%" if @campaign.progress >= 100 page.alert "#{@campaign.total} messages delivered." else page << remote_function(:url => campaign_url, :method => :get) end end end end The first action, create, receives a POST from an Ajax form, creates a new Campaign model, fires the start method to kick off a background process, and renders RJS back to the browser. The RJS result instructs the browser to create a new Ajax request, this time to the show action. The purpose of show is to continuously poll the status of the background job. It will look up the campaign by ID and retrieve its progressa value between 0 and 100representing the percent of the job finished. Then it uses RJS to update a progressbar DIV, first adjusting its width and then inserting a textual representation of the progress. The view remains very simple, just an Ajax form to POST to the create action, and a small DIV to serve as the progress bar: <%= form_remote_tag :url => campaigns_url %> <%= submit_tag 'Send Campaign' %> <div id='progressbar' style="width: 1px; height: 16px; color: white; overflow: hidden; background-color: #610; text-align: center"> </div> <%= end_form_tag %> All tied together, the result is a pleasant Ajax solution for working long-running, server-side processes. For more information about installing and using BackgrounDRb see http://backgroundrb.rubyforge.org. |