Starling and Facebook

07/30/2008

It’s really important to make sure that your Rails controllers complete their actions quickly. If you have some code in their that takes a while to execute (say, a remote network call), you need to have that stuff done in a different process or thread.

For Daily Caption, we use Starling to queue up messages that need to get sent to Facebook. Another program, creatively called “facebook_daemon” watches the Starling queue.

So, you’ll want to install starling and memcached in the usual fashion.

Then, in config/initializers/starling.rb, put:
ports = { 'production' => "memcache-server:22122", "development" => "localhost:22122", "test" => "localhost:22122" }
STARLING = MemCache.new(ports[RAILS_ENV])
To add stuff to the Starling queue, you can do stuff like:
STARLING.set "key-of-some-sort", "value" 
In our FacebookPublisher model, we have the following method:
class FacebookPublisher > Facebooker::Rails::Publisher
  def self.queue action, args
    STARLING.set "facebook_actions", [action, *args]
  end
  # and more
If we need to update Facebook, say, with a updated comment notification, the following code does the trick:
FacebookPublisher.queue(:deliver_notify_caption_comment, self.caption, self)

This adds, to the key “facebook_actions”, an array that looks like this: [:deliver_notify_caption_comment, [self.caption, self]]

You following? At this point, we have that array stored in Starling. Now, it’s up to another program to monitor Starling and pop messages off its queue.

#!/usr/bin/env ruby
require 'rubygems'
require 'daemons'
require File.dirname(__FILE__) + "/../config/environment" 

class FacebookDaemon
  TIMEOUT = 5

  def self.do_publisher_things args
    puts "Running FB publisher" 
    puts args.inspect
    action = args.first
    args = args[1..-1]
    Timeout.timeout(TIMEOUT) { FacebookPublisher.send action, *args}
  end

  def self.do_profile_updates args
    puts "Doing profile update" 
    puts args.inspect
    user, fb_user, content = args
    Timeout.timeout(TIMEOUT) { fb_user.profile_fbml = content }
    Timeout.timeout(TIMEOUT) { user.update_friends! }
  end

  def self.update
    begin
      while fetch = STARLING.get("facebook_actions")         
        do_publisher_things(fetch) 
      end
      while fetch = STARLING.get("facebook_profile_update") 
        do_profile_updates(fetch)  
      end
    rescue StandardError => e
      puts "**** EXCEPTION!!! fetch = #{ fetch.inspect } *** " 
      puts e
      puts e.backtrace.inspect
    end
  end
end

ActiveRecord::Base.logger = Logger.new STDOUT

options = {
  :app_name => "facebook_daemon",
  :ARGV => ARGV,
  :dir_mode => :normal,
  :dir => File.expand_path(File.dirname(__FILE__) + '/../log'),
  :multiple => false,
  :backtrace => true,
  :monitor => false
}

Daemons.run_proc("facebook_daemon", options) do
  loop do
    FacebookDaemon.update
    sleep 1
  end
end

It’s pretty straightforward stuff, FacebookDaemon.update is called once a second. The update method pops stuff off the Starling queue, and then calls do_publisher_things with whatever array was stored. do_publisher_things knows that the method is the first thing in the array, so calls that method. Hence, FacebookPublisher.deliver_notify_caption_comment (or whatever is on the queue to be called) is called, and Facebook gets sent the call.

If you keep the slow code out of the Rails request cycle, everyone will be happy. I’ve outlined one strategy, but there are more. We also use BackgroundJob to move slow stuff out of the cycle.

No comments yet.