Last Updated: September 29, 2021
·
2.297K
· lacides

HTTP Caching with ActiveResource

When developing REST clients with ActiveResource you need to remember that you are not in fact working with a database connection. For example, there’s a lot going on to make a find method work:

  • Sending the GET request.

  • Whatever the server needs to do to process the request, which may include getting data from a database, serializing the response, and any extra work the server needs to do.

  • Sending the response over the network.

  • The client de-serializing the data and returning a new instance of the model.

One way of reducing the amount of unnecessary work is to implement a read-through cache. Doing so greatly increases the performance but introduces another problem: invalid caches. That is, if the api makes changes to a resource then we’re stuck with the an invalid cached resource.

Thankfully, HTTP comes with a method to invalidate caches: Etag headers.

How it works

When the server returns a resource it will also send its Etag (typically a hash generated from its last update date) on a HTTP “Etag” header.

The client then caches both the resource and its Etag. When the client sends a request to get the same resource it will also send its cached Etag on a “If-None-Match” header.

The server then compares the received "If-None-Match" with the current Etag. If they match that means that the client's cached resource is valid and the server will respond with a very short 304 (Not Modified) response that won't include the resource contents, if they don't match then it will respond with a full response that includes the resource contents.

If the client gets the 304 response then it will use its cached resource, if not it will update its cache with the new resource and use it.

Here I’ll show you how to to implement caching on active support and how to use Etags to invalidate the cache.

The Server

require "sinatra"
require "sinatra/activerecord"

set :database, "sqlite3:///foo.sqlite3"

class User < ActiveRecord::Base
end

get '/users/:id.json' do
  @user = User.find(params[:id])
  etag @user.updated_at
  sleep 4
  content_type :json
  @user.to_json
end

put '/users/:id.json' do
  @user = User.find(params[:id])
  @user.touch(:updated_at)
  content_type :json
  @user.to_json  
end

This is a very simple API on sinatra. To respond a request to get an user it will just load it from the db, simulate some work by waiting 4 seconds and then return a json representation of the user.
The magic is on the call to sinatra's entity_tag method (aliased as etag) which handles the server side implementation of etag caching.

The Client

class User < ActiveResource::Base
end

The Cache

module ActiveResourceCaching
  extend  ActiveSupport::Concern

  included do
    class_attribute :cache
    self.cache = nil
  end

  module ClassMethods
    def cache_with(*store_option)
      self.cache = ActiveSupport::Cache.lookup_store(store_option)
      self.alias_method_chain :get, :cache
    end
  end

  def get_with_cache(path, headers = {})
    cached_resource = self.cache.read(path)
    response = if cached_resource && cached_etag = cached_resource["Etag"]
      get_without_cache(path, headers.merge("If-None-Match" => cached_etag))
    else
      get_without_cache(path, headers)
    end
    return cached_resource if response.code == "304"
    self.cache.write(path, response)    
    response
  end
end

ActiveResource::Connection.send :include, ActiveResourceCaching
ActiveResource::Connection.cache_with :file_store, '/tmp/cache'

This an implementation of a read-through cache sitting on top of active resource's connection class.
The beauty of it is that, thanks to active resource awesomeness, you can use any cache store you want.