Martyn Loughran

Scraps of life…
Islington, London
Cell 07966632396

New Bamboo

APICache

You want to use the Twitter API but you don't want to die? I have the solution to API caching:

APICache.get("http://twitter.com/statuses/public_timeline.rss")

You get the following functionality for free:

  • New data every 10 minutes
  • If the twitter API dies then keep using the last data received for a day. Then assume it's invalid and announce that Twitter has FAILED (optional).
  • Don't hit the rate limit (70 requests per 60 minutes)

So what exactly does APICache do? Given cached data less than 10 minutes old, it returns that. Otherwise, assuming it didn't try to request the URL within the last minute (to avoid the rate limit), it makes a get request to the Twitter API. If the Twitter API timeouts or doesn't return a 2xx code (very likely) we're still fine: it just returns the last data fetched (as long as it's less than a day old). In the exceptional case that all is lost and no data can be returned, it raises an APICache::NotAvailableError exception. You're responsible for catching this exception and complaining bitterly to the internet.

All very simple. What if you need to do something more complicated? Say you need authentication or the silly API you're using doesn't follow a nice convention of returning 2xx for success. Then you need a block:

APICache.get('twitter_replies', :cache => 3600) do
  Net::HTTP.start('twitter.com') do |http|
    req = Net::HTTP::Get.new('/statuses/replies.xml')
    req.basic_auth 'username', 'password'
    response = http.request(req)
    case response
    when Net::HTTPSuccess
      # 2xx response code
      response.body
    else
      raise APICache::Invalid
    end
  end
end

All the caching is still handled for you. If you supply a block then the first argument to APICache.get is assumed to be a unique key rather than a URL. Throwing APICache::Invalid signals to APICache that the request was not successful.

You can send any of the following options to APICache.get(url, options = {}, &block). These are the default values (times are all in seconds):

{
  :cache => 600,    # 10 minutes  After this time fetch new data
  :valid => 86400,  # 1 day       Maximum time to use old data
                    #             :forever is a valid option
  :period => 60,    # 1 minute    Maximum frequency to call API
  :timeout => 5     # 5 seconds   API response timeout
}

Before using the APICache you need to initialize the caches. In merb, for example, put this in your init.rb:

APICache.start

Currently there are two stores available: MemcacheStore and MemoryStore. MemcacheStore is the default but if you'd like to use MemoryStore, or another store - see AbstractStore, just supply it to the start method:

APICache.start(APICache::MemoryStore)

I suppose you'll want to get your hands on this magic. For now just grab the source from http://github.com/mloughran/api_cache/tree/master and rake install. I'll get a gem sorted soon.

This is the irb quickstart so that you don't have to re-parse the above:

require 'rubygems'
require 'api_cache'
APICache.start(APICache::MemoryStore)
APICache.get("http://twitter.com/statuses/public_timeline.rss")

Please send feedback if you think of any other functionality that would be handy.

Nested layouts in Merb

First a simple solution using throw_content and catch_content.

This is what sublayout.html.erb looks like

<% throw_content :sublayout do %>
  <div class="header">
    ...
  </div>

  <%= catch_content :for_layout %>

  <div class="footer">
    ...
  </div>
<% end %>

<%= render catch_content(:sublayout), :layout => "application" %>

Your main layout contains <%= catch_content :for_layout %> as usual.


You might find it handy to use catch and throw content in a few more places. For example, in your sublayout you could <%= catch_content :sublayout_title %> and throw it in the view with

<% throw_content :sublayout_title %>
  Hippos
<% end %>

As a little bonus I wrote a helper so that you can use the same syntax as you're used to from the rails plugin. Just add this snippet into Merb::GlobalHelpers

def inside_layout(layout, &block)
  content = render(capture(&block), :layout => layout)
  concat(content, block.binding)
end

Then your sublayout looks like

<% inside_layout :sublayout do %>
  <div class="header">
    ...
  </div>

  <%= catch_content :for_layout %>

  <div class="footer">
    ...
  </div>
<% end %>

Here's another idea which I think would be even better: What if you could say layout ['application', 'sublayout'] in your controller and have each layout rendered inside it's parent all up the chain? Then sublayout.html.erb wouldn't be constrained to always render inside application.html.erb and you could use it in a much more flexible way. However Damien things it's a mad idea. What do you think?

Freeze gems in a Merb application

Frozen gems in Merb == gems installed inside the application.

That's clever because now you can use the standard tools for installing and managing the installed gems. For example, you get dependency checking and so on for free. Add -i gems to the command line options to specify the install location.

gem install merb-core -i gems

That's the simplest case (run it from the root of you merb app). It fetches the merb-core gem from the gem servers and installs it inside a gems directory.

However, in many cases that's not what you want. Really you want to freeze the version you're currently developing on. Maybe one you've installed from the merb-core git repository. That's really easy because you have the gem package files on your system already. For me they're in /Library/Ruby/Gems/1.8/cache. Get your location by running gem environment gemdir and look for a cache folder. Install the package with

gem install /Library/Ruby/Gems/1.8/cache/merb-core-0.9.1.gem -i gems

You should add /gems/doc to your .gitignore since you definitely don't want ri and rdoc files in your repository. Alternatively add --no-ri --no-rdoc to the gem install command.

The final challenge is actually getting merb started. <strike>Right now I'm relying on a pastie that google found for me (I'm sorry I have no idea who the author is). Make that executable and use it to run merb with the usual merb command line options. That's surely a bit of a hack though and I'm hoping that someone will point out a better way :)</strike> Update: Thanks Steve - I hadn't spotted the generator. Run merb-gen freezer frozen-merb and then run Merb with ./script/frozen-merb passing the usual merb command line options.

How to use git / github with capistrano

I'm assuming that you're using private key authentication for git and that you've got an ssh agent set up (if you're running Leopard then you automatically have this).

The trick is to use ssh agent forwarding, now supported in capistrano. It allows the server to pull the latest code from github using your local private key and ssh agent. To understand how this trickery works take a look at this illustrated guide.

set :ssh_options, { :forward_agent => true }

One of the great things about git is that it's really fast, even if you're cloning an entire repository. However you can go even faster by using the repository_cache option in capistrano. This essentially keeps a clone of your app on the server and then just does a git pull to fetch new changes and copies the directory across when you deploy.

set :repository_cache, "git_cache"
set :deploy_via, :remote_cache

Another recommended strategy is to specify a git branch. Otherwise you'll default to deploying HEAD which might be some crazy experiment.

set :branch, "stable"

In case you haven't used remote branches they're really easy. Say you have a local stable branch. Push this to github using

git push github stable

So the complete capistrano file should look something like

set :application, "your cool app"

# Source code
set :scm, :git
set :repository, "git@github.com:githubusername/gitrepo.git"
set :branch, "stable"
set :repository_cache, "git_cache"
set :deploy_via, :remote_cache
set :ssh_options, { :forward_agent => true }

# Deployment servers
role :app, "your server"
role :web, "your server"
role :db,  "your server", :primary => true
set :deploy_to, "/var/www/#{application}"