Section 9.5. Dealing with Long-Running Tasks


9.5. Dealing with Long-Running Tasks

When 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.




Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net