Cache HTTP com ActiveResource

Ao desenvolver clientes REST com ActiveResource, você precisa se lembrar de que não está de fato trabalhando com uma conexão de banco de dados. Por exemplo, há muita coisa acontecendo para fazer um método localizar funcionar:

  • Enviando a solicitação GET.

  • O que quer que o servidor precise fazer para processar a solicitação, o que pode incluir obter dados de um banco de dados, serializar a resposta e qualquer trabalho extra que o servidor precise fazer.

  • Enviando a resposta pela rede.

  • O cliente desserializa os dados e retorna uma nova instância do modelo.

Uma maneira de reduzir a quantidade de trabalho desnecessário é implementar um cache de leitura. Fazer isso aumenta bastante o desempenho, mas apresenta outro problema: caches inválidos. Ou seja, se a API fizer alterações em um recurso, ficaremos presos a um recurso armazenado em cache inválido.

Felizmente, o HTTP vem com um método para invalidar caches: cabeçalhos Etag.

Como funciona

Quando o servidor retorna um recurso, ele também envia seu Etag (normalmente um hash gerado a partir da data da última atualização) em um cabeçalho HTTP “Etag”.

O cliente então armazena em cache o recurso e seu Etag. Quando o cliente envia uma solicitação para obter o mesmo recurso, ele também envia seu Etag em cache em um cabeçalho “If-None-Match”.

O servidor então compara o “If-None-Match” recebido com o Etag atual. Se eles corresponderem, isso significa que o recurso em cache do cliente é válido e o servidor responderá com uma resposta 304 (não modificada) muito curta que não incluirá o conteúdo do recurso; se eles não corresponderem, ele responderá com uma resposta completa que inclui o conteúdo do recurso.

Se o cliente obtiver a resposta 304, ele usará seu recurso em cache; caso contrário, ele atualizará seu cache com o novo recurso e o usará.

Aqui, vou mostrar como implementar o cache no suporte ativo e como usar Etags para invalidar o cache.

O servidor

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

Esta é uma API muito simples no sinatra. Para responder a uma solicitação para obter um usuário, basta carregá-lo do banco de dados, simular algum trabalho aguardando 4 segundos e, em seguida, retornar uma representação json do usuário.
A mágica está na chamada para o método entity_tag do sinatra (apelidado de etag), que lida com a implementação do servidor de cache etag.

O cliente

class User < ActiveResource::Base
end

O 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'

Esta é uma implementação de um cache de leitura localizado no topo da classe de conexão do recurso ativo.
A beleza disso é que, graças à grandiosidade dos recursos ativos, você pode usar qualquer armazenamento de cache que desejar.